diff --git a/python/tracing/chroma/_shared.py b/python/tracing/chroma/_shared.py index f6ae5fc..2aae7ec 100644 --- a/python/tracing/chroma/_shared.py +++ b/python/tracing/chroma/_shared.py @@ -53,6 +53,7 @@ def load_example_env() -> None: def create_respan(workflow_name: str) -> Respan: load_example_env() + run_id = os.getenv("RESPAN_EXAMPLE_RUN_ID") or uuid4().hex[:8] return Respan( api_key=os.environ["RESPAN_API_KEY"], base_url=os.getenv("RESPAN_BASE_URL", RESPAN_BASE_URL), @@ -60,6 +61,7 @@ def create_respan(workflow_name: str) -> Respan: metadata={ "example_set": EXAMPLE_SET, "workflow_name": workflow_name, + "run_id": run_id, }, instrumentations=[ChromaInstrumentor()], is_batching_enabled=False, @@ -82,6 +84,7 @@ def workflow_attributes(workflow_name: str) -> dict[str, object]: "example_set": EXAMPLE_SET, "workflow_name": workflow_name, "example_run_id": run_id, + "run_id": run_id, }, } diff --git a/python/tracing/huggingface/04_real_tiny_pipeline.py b/python/tracing/huggingface/04_real_tiny_pipeline.py new file mode 100644 index 0000000..e948229 --- /dev/null +++ b/python/tracing/huggingface/04_real_tiny_pipeline.py @@ -0,0 +1,87 @@ +"""Trace an actual local Transformers pipeline without downloading a model.""" + +from __future__ import annotations + +import torch +from respan import workflow +from tokenizers import Tokenizer +from tokenizers.models import WordLevel +from tokenizers.pre_tokenizers import Whitespace +from transformers import ( + GPT2Config, + GPT2LMHeadModel, + PreTrainedTokenizerFast, + TextGenerationPipeline, +) + +from _shared import build_respan, print_result + +EXAMPLE_NAME = "real-tiny-pipeline" +WORKFLOW_NAME = "huggingface_04_real_tiny_pipeline" + + +def build_pipeline() -> TextGenerationPipeline: + vocabulary = { + "[PAD]": 0, + "[UNK]": 1, + "[EOS]": 2, + "Tracing": 3, + "Hugging": 4, + "Face": 5, + "locally": 6, + "keeps": 7, + "telemetry": 8, + "repeatable": 9, + ".": 10, + } + tokenizer_backend = Tokenizer( + WordLevel(vocab=vocabulary, unk_token="[UNK]") + ) + tokenizer_backend.pre_tokenizer = Whitespace() + tokenizer = PreTrainedTokenizerFast( + tokenizer_object=tokenizer_backend, + unk_token="[UNK]", + pad_token="[PAD]", + eos_token="[EOS]", + ) + + torch.manual_seed(17) + model = GPT2LMHeadModel( + GPT2Config( + vocab_size=len(vocabulary), + name_or_path="respan-local-tiny-gpt2", + n_positions=32, + n_ctx=32, + n_embd=16, + n_layer=1, + n_head=1, + bos_token_id=2, + eos_token_id=2, + pad_token_id=0, + ) + ) + model.eval() + return TextGenerationPipeline(model=model, tokenizer=tokenizer, device=-1) + + +@workflow(name=WORKFLOW_NAME) +def execute_real_tiny_generation() -> list[dict[str, str]]: + return build_pipeline()( + "Tracing Hugging Face locally", + max_new_tokens=4, + do_sample=False, + ) + + +def run() -> list[dict[str, str]]: + respan = build_respan(example_name=EXAMPLE_NAME, workflow_name=WORKFLOW_NAME) + try: + result = execute_real_tiny_generation() + print_result("real_tiny_generation", result) + return result + finally: + respan.shutdown() + + +if __name__ == "__main__": + run() diff --git a/python/tracing/huggingface/README.md b/python/tracing/huggingface/README.md index da6f4b4..8b97b2c 100644 --- a/python/tracing/huggingface/README.md +++ b/python/tracing/huggingface/README.md @@ -31,3 +31,4 @@ python run_all.py | `01_text_generation_pipeline.py` | `huggingface_01_text_generation_pipeline` | Single `TextGenerationPipeline.__call__`, model metadata, generation parameters, prompt, and completion | | `02_batch_prompts.py` | `huggingface_02_batch_prompts` | Batch prompt indexing and multiple generated completions | | `03_trace_content_disabled.py` | `huggingface_03_trace_content_disabled` | `TRACELOOP_TRACE_CONTENT=false` privacy mode with metadata-only generation spans | +| `04_real_tiny_pipeline.py` | `huggingface_04_real_tiny_pipeline` | Real local `transformers.TextGenerationPipeline` with an in-memory tokenizer and randomly initialized tiny GPT-2 model; no model download or provider credential | diff --git a/python/tracing/huggingface/requirements.txt b/python/tracing/huggingface/requirements.txt index 7c9242c..5f10a24 100644 --- a/python/tracing/huggingface/requirements.txt +++ b/python/tracing/huggingface/requirements.txt @@ -1,3 +1,6 @@ -python-dotenv -respan-ai -respan-instrumentation-huggingface +opentelemetry-instrumentation-transformers>=0.61.0 +python-dotenv>=1.0.0 +respan-ai>=2.17.0 +respan-instrumentation-huggingface>=0.1.0 +torch>=2.2.0 +transformers>=4.0 diff --git a/python/tracing/huggingface/run_all.py b/python/tracing/huggingface/run_all.py index 0fd77f0..ff76679 100644 --- a/python/tracing/huggingface/run_all.py +++ b/python/tracing/huggingface/run_all.py @@ -11,6 +11,7 @@ "01_text_generation_pipeline.py", "02_batch_prompts.py", "03_trace_content_disabled.py", + "04_real_tiny_pipeline.py", ] diff --git a/python/tracing/instructor/01_create.py b/python/tracing/instructor/01_create.py index eb38f6b..8419a44 100644 --- a/python/tracing/instructor/01_create.py +++ b/python/tracing/instructor/01_create.py @@ -4,11 +4,10 @@ from typing import TypedDict +from _respan_instructor import create_respan_instructor_client from respan_tracing import workflow from respan_tracing.exporters import propagate_attributes -from _respan_instructor import create_respan_instructor_client - class InvoiceSummary(TypedDict): vendor: str @@ -19,7 +18,7 @@ class InvoiceSummary(TypedDict): @workflow(name="instructor_example_01_create") -def extract_invoice(client) -> InvoiceSummary: +def extract_invoice(client, scenario: str) -> InvoiceSummary: return client.create( response_model=InvoiceSummary, messages=[ @@ -37,15 +36,17 @@ def extract_invoice(client) -> InvoiceSummary: def run_create_example() -> None: - telemetry, client = create_respan_instructor_client(app_name="instructor-create") - - with propagate_attributes( - thread_identifier="instructor_example_01_create", - metadata={"example_script": "01_create.py", "instructor_api": "create"}, - ): - invoice = extract_invoice(client) - - print(dict(invoice)) + respan, client = create_respan_instructor_client(app_name="instructor-create") + try: + with propagate_attributes( + thread_identifier="instructor_example_01_create", + metadata={"example_script": "01_create.py", "instructor_api": "create"}, + ): + invoice = extract_invoice(client, "extract a deterministic invoice") + + print(dict(invoice)) + finally: + respan.shutdown() if __name__ == "__main__": diff --git a/python/tracing/instructor/02_validation_hooks.py b/python/tracing/instructor/02_validation_hooks.py index b7b99bb..023ee9d 100644 --- a/python/tracing/instructor/02_validation_hooks.py +++ b/python/tracing/instructor/02_validation_hooks.py @@ -2,15 +2,13 @@ from __future__ import annotations -from typing import Literal -from typing import TypedDict +from typing import Literal, TypedDict +from _respan_instructor import create_respan_instructor_client from instructor.core.hooks import HookName from respan_tracing import workflow from respan_tracing.exporters import propagate_attributes -from _respan_instructor import create_respan_instructor_client - class SupportEscalation(TypedDict): customer: str @@ -21,7 +19,7 @@ class SupportEscalation(TypedDict): @workflow(name="instructor_example_02_validation_hooks") -def classify_support_escalation(client) -> SupportEscalation: +def classify_support_escalation(client, scenario: str) -> SupportEscalation: return client.create( response_model=SupportEscalation, max_retries=2, @@ -39,7 +37,7 @@ def classify_support_escalation(client) -> SupportEscalation: def run_validation_hooks_example() -> None: - telemetry, client = create_respan_instructor_client( + respan, client = create_respan_instructor_client( app_name="instructor-validation-hooks" ) hook_counts = {"completion_kwargs": 0, "completion_response": 0} @@ -61,21 +59,27 @@ def on_completion_response(_response) -> None: client.on(HookName.COMPLETION_RESPONSE, on_completion_response) try: - with propagate_attributes( - customer_identifier="customer_instructor_example", - thread_identifier="instructor_example_02_validation_hooks", - metadata={ - "example_script": "02_validation_hooks.py", - "instructor_api": "create_hooks", - }, - ): - escalation = classify_support_escalation(client) + try: + with propagate_attributes( + customer_identifier="customer_instructor_example", + thread_identifier="instructor_example_02_validation_hooks", + metadata={ + "example_script": "02_validation_hooks.py", + "instructor_api": "create_hooks", + }, + ): + escalation = classify_support_escalation( + client, + "classify a deterministic support escalation", + ) + finally: + client.off(HookName.COMPLETION_KWARGS, on_completion_kwargs) + client.off(HookName.COMPLETION_RESPONSE, on_completion_response) + + print(dict(escalation)) + print(hook_counts) finally: - client.off(HookName.COMPLETION_KWARGS, on_completion_kwargs) - client.off(HookName.COMPLETION_RESPONSE, on_completion_response) - - print(dict(escalation)) - print(hook_counts) + respan.shutdown() if __name__ == "__main__": diff --git a/python/tracing/instructor/03_create_with_completion.py b/python/tracing/instructor/03_create_with_completion.py index 720e739..1ddace8 100644 --- a/python/tracing/instructor/03_create_with_completion.py +++ b/python/tracing/instructor/03_create_with_completion.py @@ -1,15 +1,13 @@ -"""Return the parsed model and the raw provider completion.""" +"""Return the parsed model and a bounded provider completion summary.""" from __future__ import annotations -from typing import Literal -from typing import TypedDict +from typing import Literal, TypedDict +from _respan_instructor import create_respan_instructor_client from respan_tracing import workflow from respan_tracing.exporters import propagate_attributes -from _respan_instructor import create_respan_instructor_client - class ReleaseNote(TypedDict): title: str @@ -19,8 +17,8 @@ class ReleaseNote(TypedDict): @workflow(name="instructor_example_03_create_with_completion") -def draft_release_note(client) -> tuple[ReleaseNote, object]: - return client.create_with_completion( +def draft_release_note(client, scenario: str) -> tuple[ReleaseNote, dict[str, object]]: + release_note, completion = client.create_with_completion( response_model=ReleaseNote, messages=[ { @@ -33,30 +31,35 @@ def draft_release_note(client) -> tuple[ReleaseNote, object]: } ], ) + return release_note, { + "completion_type": type(completion).__name__, + "completion_id": getattr(completion, "id", None), + "completion_model": getattr(completion, "model", None), + } def run_create_with_completion_example() -> None: - telemetry, client = create_respan_instructor_client( + respan, client = create_respan_instructor_client( app_name="instructor-create-with-completion" ) - with propagate_attributes( - thread_identifier="instructor_example_03_create_with_completion", - metadata={ - "example_script": "03_create_with_completion.py", - "instructor_api": "create_with_completion", - }, - ): - release_note, completion = draft_release_note(client) - - print(dict(release_note)) - print( - { - "completion_type": type(completion).__name__, - "completion_id": getattr(completion, "id", None), - "completion_model": getattr(completion, "model", None), - } - ) + try: + with propagate_attributes( + thread_identifier="instructor_example_03_create_with_completion", + metadata={ + "example_script": "03_create_with_completion.py", + "instructor_api": "create_with_completion", + }, + ): + release_note, completion_summary = draft_release_note( + client, + "draft a deterministic release note", + ) + + print(dict(release_note)) + print(completion_summary) + finally: + respan.shutdown() if __name__ == "__main__": diff --git a/python/tracing/instructor/04_create_iterable.py b/python/tracing/instructor/04_create_iterable.py index b88199a..e896d2a 100644 --- a/python/tracing/instructor/04_create_iterable.py +++ b/python/tracing/instructor/04_create_iterable.py @@ -2,14 +2,12 @@ from __future__ import annotations -from typing import Literal -from typing import TypedDict +from typing import Literal, TypedDict +from _respan_instructor import create_respan_instructor_client from respan_tracing import workflow from respan_tracing.exporters import propagate_attributes -from _respan_instructor import create_respan_instructor_client - class ActionItem(TypedDict): owner: str @@ -19,7 +17,7 @@ class ActionItem(TypedDict): @workflow(name="instructor_example_04_create_iterable") -def extract_action_items(client) -> list[ActionItem]: +def extract_action_items(client, scenario: str) -> list[ActionItem]: return list( client.create_iterable( response_model=ActionItem, @@ -39,20 +37,26 @@ def extract_action_items(client) -> list[ActionItem]: def run_create_iterable_example() -> None: - telemetry, client = create_respan_instructor_client( + respan, client = create_respan_instructor_client( app_name="instructor-create-iterable" ) - with propagate_attributes( - thread_identifier="instructor_example_04_create_iterable", - metadata={ - "example_script": "04_create_iterable.py", - "instructor_api": "create_iterable", - }, - ): - action_items = extract_action_items(client) - - print([dict(item) for item in action_items]) + try: + with propagate_attributes( + thread_identifier="instructor_example_04_create_iterable", + metadata={ + "example_script": "04_create_iterable.py", + "instructor_api": "create_iterable", + }, + ): + action_items = extract_action_items( + client, + "extract three deterministic action items", + ) + + print([dict(item) for item in action_items]) + finally: + respan.shutdown() if __name__ == "__main__": diff --git a/python/tracing/instructor/05_async_create.py b/python/tracing/instructor/05_async_create.py index ac0f0cb..16725ac 100644 --- a/python/tracing/instructor/05_async_create.py +++ b/python/tracing/instructor/05_async_create.py @@ -5,11 +5,10 @@ import asyncio from typing import TypedDict +from _respan_instructor import create_respan_instructor_client from respan_tracing import workflow from respan_tracing.exporters import propagate_attributes -from _respan_instructor import create_respan_instructor_client - class ProjectBrief(TypedDict): title: str @@ -19,7 +18,7 @@ class ProjectBrief(TypedDict): @workflow(name="instructor_example_05_async_create") -async def create_project_brief(client) -> ProjectBrief: +async def create_project_brief(client, scenario: str) -> ProjectBrief: return await client.create( response_model=ProjectBrief, messages=[ @@ -37,21 +36,27 @@ async def create_project_brief(client) -> ProjectBrief: async def run_async_create_example() -> None: - telemetry, client = create_respan_instructor_client( + respan, client = create_respan_instructor_client( app_name="instructor-async-create", async_client=True, ) - with propagate_attributes( - thread_identifier="instructor_example_05_async_create", - metadata={ - "example_script": "05_async_create.py", - "instructor_api": "async_create", - }, - ): - project_brief = await create_project_brief(client) - - print(dict(project_brief)) + try: + with propagate_attributes( + thread_identifier="instructor_example_05_async_create", + metadata={ + "example_script": "05_async_create.py", + "instructor_api": "async_create", + }, + ): + project_brief = await create_project_brief( + client, + "build a deterministic project brief", + ) + + print(dict(project_brief)) + finally: + respan.shutdown() if __name__ == "__main__": diff --git a/python/tracing/instructor/README.md b/python/tracing/instructor/README.md index d0674ef..09cdb09 100644 --- a/python/tracing/instructor/README.md +++ b/python/tracing/instructor/README.md @@ -1,7 +1,7 @@ # Instructor Respan Integration Examples These examples exercise Instructor's own client functions while routing traffic -through the Respan gateway. Tracing is initialized with `respan-tracing`, and +through the Respan gateway. Tracing is initialized with `respan`, and Instructor-specific spans come from `respan-instrumentation-instructor`. ## Setup @@ -28,7 +28,8 @@ separate OpenAI key is required. Each script wraps its Instructor call in a uniquely named Respan workflow and propagates `example_script` metadata, so traces are easy to filter alongside -the Instructor chat span. +the Instructor chat span. `RESPAN_EXAMPLE_RUN_ID` is propagated to every span, +and every script shuts down Respan in `finally`. By default the examples use `gpt-4o-mini`. Override it with: @@ -52,6 +53,12 @@ Run any example: python 01_create.py ``` +Run the complete maintained set: + +```bash +python run_all.py +``` + ## Further Reading - [Instructor](https://python.useinstructor.com/) diff --git a/python/tracing/instructor/_respan_instructor.py b/python/tracing/instructor/_respan_instructor.py index 27707f3..7be967b 100644 --- a/python/tracing/instructor/_respan_instructor.py +++ b/python/tracing/instructor/_respan_instructor.py @@ -3,34 +3,42 @@ from __future__ import annotations import os +from pathlib import Path from typing import Any import instructor -from dotenv import find_dotenv from dotenv import load_dotenv -from respan_tracing import RespanTelemetry +from respan import Respan from respan_instrumentation_instructor import InstructorInstrumentor +REPO_ROOT = Path(__file__).resolve().parents[3] + def create_respan_instructor_client( *, app_name: str, async_client: bool = False, -) -> tuple[RespanTelemetry, Any]: +) -> tuple[Respan, Any]: """Create an Instructor client routed through the Respan gateway.""" - load_dotenv(find_dotenv(), override=True) + load_dotenv(REPO_ROOT / ".env", override=True) respan_api_key = os.environ["RESPAN_API_KEY"] respan_base_url = os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api") model = os.getenv("INSTRUCTOR_MODEL", "gpt-4o-mini") - telemetry = RespanTelemetry( + run_id = os.getenv("RESPAN_EXAMPLE_RUN_ID", "instructor-local") + respan = Respan( api_key=respan_api_key, base_url=respan_base_url, app_name=app_name, - is_auto_instrument=False, + instrumentations=[InstructorInstrumentor()], + metadata={ + "example_set": "instructor", + "run_id": run_id, + }, + environment="examples", + is_batching_enabled=False, ) - InstructorInstrumentor().activate() client = instructor.from_provider( f"openai/{model}", api_key=respan_api_key, @@ -38,4 +46,4 @@ def create_respan_instructor_client( async_client=async_client, mode=instructor.Mode.TOOLS, ) - return telemetry, client + return respan, client diff --git a/python/tracing/instructor/async_instructor_example.py b/python/tracing/instructor/async_instructor_example.py deleted file mode 100644 index 7e8e45d..0000000 --- a/python/tracing/instructor/async_instructor_example.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Simple Async Instructor + Respan Tracing Example - -This example shows how easy it is to add Respan tracing to your async Instructor workflows. -Just 3 lines of setup, then your structured outputs are automatically traced! -""" - -import asyncio -import os -from pydantic import BaseModel, Field -import instructor -from openai import AsyncOpenAI -from respan_tracing import RespanTelemetry, Instruments -from respan_tracing.decorators import task -from dotenv import load_dotenv -load_dotenv() - -# 1️⃣ Initialize Respan tracing (one line!) -k_tl = RespanTelemetry(app_name="async-instructor-demo", instruments={Instruments.OPENAI}) - -# 2️⃣ Set up your async Instructor client (your existing code) -async_client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) -instructor_client = instructor.from_openai(async_client) - -# 3️⃣ Define your Pydantic models (your existing code) -class User(BaseModel): - name: str = Field(description="Full name") - age: int = Field(description="Age in years") - email: str = Field(description="Email address") - role: str = Field(description="Job title") - -# 4️⃣ Add @task decorator to your functions (one line per function!) -@task(name="extract_user_async") -async def extract_user(text: str) -> User: - """Extract user information using async Instructor.""" - return await instructor_client.chat.completions.create( - model="gpt-4o-mini", - response_model=User, - messages=[ - {"role": "system", "content": "Extract user information from the text."}, - {"role": "user", "content": text} - ], - temperature=0.1 - ) - -async def main(): - """Demo the async extraction with tracing.""" - - # Sample text - user_text = """ - Meet Alex Johnson, a 32-year-old Senior Software Engineer at Google. - You can reach Alex at alex.johnson@google.com for any technical questions. - """ - - # Extract user (automatically traced!) - user = await extract_user(user_text) - - print(user.model_dump()) - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/python/tracing/instructor/requirements.txt b/python/tracing/instructor/requirements.txt index 9b22d2e..2fc49f5 100644 --- a/python/tracing/instructor/requirements.txt +++ b/python/tracing/instructor/requirements.txt @@ -1,5 +1,6 @@ -respan-tracing -respan-instrumentation-instructor +respan-ai>=2.17.0 +respan-tracing>=2.17.0 +respan-instrumentation-instructor>=0.1.0 instructor>=1.3.7 openai>=2.0.0 python-dotenv>=1.0.1 diff --git a/python/tracing/instructor/run_all.py b/python/tracing/instructor/run_all.py new file mode 100644 index 0000000..94f1ad2 --- /dev/null +++ b/python/tracing/instructor/run_all.py @@ -0,0 +1,30 @@ +"""Run all maintained Instructor examples in isolated processes.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parent +SCRIPTS = [ + "01_create.py", + "02_validation_hooks.py", + "03_create_with_completion.py", + "04_create_iterable.py", + "05_async_create.py", +] + + +def main() -> None: + for script in SCRIPTS: + print(f"\n== Running {script} ==", flush=True) + subprocess.run( + [sys.executable, str(EXAMPLE_DIR / script)], + cwd=EXAMPLE_DIR, + check=True, + ) + + +if __name__ == "__main__": + main()