Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions python/tracing/mirascope/01_call_and_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Run a real Mirascope Model.call and Toolkit.execute with deterministic data."""

from __future__ import annotations

import json

from _shared import (
close_model_provider,
create_deterministic_model,
create_respan,
finish_respan,
workflow_attributes,
)
from mirascope import llm
from respan import workflow

WORKFLOW_NAME = "mirascope-call-and-tool"


@llm.tool
def lookup_weather(city: str) -> dict[str, object]:
"""Return deterministic weather for a city."""
return {"city": city, "temperature_c": 18, "conditions": "sunny"}


def create_runner(model: llm.Model):
@workflow(name=WORKFLOW_NAME)
def run_call_and_tool(city: str) -> dict[str, object]:
response = model.call(
f"Use lookup_weather for {city}.",
tools=[lookup_weather],
)
outputs = response.execute_tools()
return {
"assistant_tool_calls": [
{"id": call.id, "name": call.name, "args": json.loads(call.args)}
for call in response.tool_calls
],
"tool_results": [output.result for output in outputs],
}

return run_call_and_tool


def main() -> None:
respan = create_respan(WORKFLOW_NAME)
model: llm.Model | None = None
try:
model = create_deterministic_model()
runner = create_runner(model)
with respan.propagate_attributes(
**workflow_attributes(WORKFLOW_NAME, "01_call_and_tool.py")
):
result = runner("Paris")
print(json.dumps(result, sort_keys=True))
finally:
try:
if model is not None:
close_model_provider(model)
finally:
finish_respan(respan)


if __name__ == "__main__":
main()
65 changes: 65 additions & 0 deletions python/tracing/mirascope/02_sync_async_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Consume real Mirascope sync and async stream response objects."""

from __future__ import annotations

import asyncio
import json

from _shared import (
close_model_provider,
create_deterministic_model,
create_respan,
finish_respan,
workflow_attributes,
)
from mirascope import llm
from respan import workflow

SYNC_WORKFLOW = "mirascope-sync-stream"
ASYNC_WORKFLOW = "mirascope-async-stream"


def create_sync_runner(model: llm.Model):
@workflow(name=SYNC_WORKFLOW)
def run_sync_stream(prompt: str) -> str:
response = model.stream(prompt)
return "".join(response.text_stream()).strip()

return run_sync_stream


def create_async_runner(model: llm.Model):
@workflow(name=ASYNC_WORKFLOW)
async def run_async_stream(prompt: str) -> str:
response = await model.stream_async(prompt)
return "".join([part async for part in response.text_stream()]).strip()

return run_async_stream


async def main() -> None:
respan = create_respan("mirascope-sync-async-stream")
model: llm.Model | None = None
try:
model = create_deterministic_model()
sync_runner = create_sync_runner(model)
async_runner = create_async_runner(model)
with respan.propagate_attributes(
**workflow_attributes(SYNC_WORKFLOW, "02_sync_async_stream.py")
):
sync_result = sync_runner("Stream the deterministic sync reply.")
with respan.propagate_attributes(
**workflow_attributes(ASYNC_WORKFLOW, "02_sync_async_stream.py")
):
async_result = await async_runner("Stream the deterministic async reply.")
print(json.dumps({"sync": sync_result, "async": async_result}, sort_keys=True))
finally:
try:
if model is not None:
close_model_provider(model)
finally:
finish_respan(respan)


if __name__ == "__main__":
asyncio.run(main())
52 changes: 52 additions & 0 deletions python/tracing/mirascope/03_expected_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Raise a deterministic Mirascope provider error across the workflow boundary."""

from __future__ import annotations

from _shared import (
close_model_provider,
create_deterministic_model,
create_respan,
finish_respan,
workflow_attributes,
)
from mirascope import llm
from respan import workflow

WORKFLOW_NAME = "mirascope-expected-provider-error"


def create_runner(model: llm.Model):
@workflow(name=WORKFLOW_NAME)
def run_expected_error(prompt: str) -> None:
model.call(prompt)

return run_expected_error


def main() -> None:
respan = create_respan(WORKFLOW_NAME)
model: llm.Model | None = None
try:
model = create_deterministic_model(fail_status=503)
runner = create_runner(model)
try:
with respan.propagate_attributes(
**workflow_attributes(WORKFLOW_NAME, "03_expected_error.py")
):
runner("Raise the deterministic provider error.")
except llm.ServerError as exc:
if exc.status_code != 503:
raise AssertionError(f"unexpected status: {exc.status_code}") from exc
print(f"expected failure ({exc.status_code}): {exc}")
else:
raise AssertionError("expected deterministic Mirascope failure")
finally:
try:
if model is not None:
close_model_provider(model)
finally:
finish_respan(respan)


if __name__ == "__main__":
main()
53 changes: 53 additions & 0 deletions python/tracing/mirascope/04_privacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Run a Mirascope call with content capture disabled."""

from __future__ import annotations

import json

from _shared import (
close_model_provider,
create_deterministic_model,
create_respan,
finish_respan,
workflow_attributes,
)
from mirascope import llm
from respan import workflow

WORKFLOW_NAME = "mirascope-content-disabled"


def create_runner(model: llm.Model):
@workflow(name=WORKFLOW_NAME)
def run_private_call(scenario: str) -> dict[str, object]:
response = model.call("private-example-content")
return {
"capture_content": False,
"response_received": bool(response.text()),
"scenario": scenario,
}

return run_private_call


def main() -> None:
respan = create_respan(WORKFLOW_NAME, capture_content=False)
model: llm.Model | None = None
try:
model = create_deterministic_model()
runner = create_runner(model)
with respan.propagate_attributes(
**workflow_attributes(WORKFLOW_NAME, "04_privacy.py")
):
result = runner("content-capture-disabled")
print(json.dumps(result, sort_keys=True))
finally:
try:
if model is not None:
close_model_provider(model)
finally:
finish_respan(respan)


if __name__ == "__main__":
main()
52 changes: 52 additions & 0 deletions python/tracing/mirascope/05_live_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Run Mirascope's OpenAI provider against the configured Respan gateway."""

from __future__ import annotations

from _shared import (
close_model_provider,
create_live_model,
create_respan,
finish_respan,
live_example_enabled,
workflow_attributes,
)
from mirascope import llm
from respan import workflow

WORKFLOW_NAME = "mirascope-live-gateway"


def create_runner(model: llm.Model):
@workflow(name=WORKFLOW_NAME)
def run_live_call(prompt: str) -> str:
response = model.call(prompt)
return response.text()

return run_live_call


def main() -> None:
if not live_example_enabled():
print("skipped live gateway; RESPAN_MIRASCOPE_RUN_LIVE=0")
return

respan = create_respan(WORKFLOW_NAME)
model: llm.Model | None = None
try:
model = create_live_model()
runner = create_runner(model)
with respan.propagate_attributes(
**workflow_attributes(WORKFLOW_NAME, "05_live_gateway.py")
):
result = runner("Reply with exactly: Mirascope live tracing works.")
print(result)
finally:
try:
if model is not None:
close_model_provider(model)
finally:
finish_respan(respan)


if __name__ == "__main__":
main()
43 changes: 43 additions & 0 deletions python/tracing/mirascope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Mirascope tracing examples

These examples exercise Mirascope 2.x model calls, sync and async streams,
tool-call execution, privacy mode, deterministic provider errors, and an
OpenAI-compatible live call through the Respan gateway.

The scripts load `RESPAN_API_KEY`, `RESPAN_BASE_URL`, and optional gateway
overrides from the repository-root `.env`. Every script adds the exact
`RESPAN_EXAMPLE_RUN_ID` to its trace metadata and explicitly flushes and shuts
down Respan.

Install the dependencies:

```bash
cd python/tracing/mirascope
pip install -r requirements.txt
```

When validating an unpublished branch, link the local packages:

```bash
pip install -e ../../../../respan/python-sdks/respan-sdk
pip install -e ../../../../respan/python-sdks/respan-tracing
pip install -e ../../../../respan/python-sdks/respan
pip install -e ../../../../respan/python-sdks/instrumentations/respan-instrumentation-mirascope
```

Run the complete set with one marker:

```bash
RESPAN_EXAMPLE_RUN_ID=otel2-fix-py-group-19-YYYYMMDDTHHMMSSZ python run_all.py
```

`05_live_gateway.py` runs by default with the repository credentials. Set
`RESPAN_MIRASCOPE_RUN_LIVE=0` to skip only that optional provider-backed call.

| Script | Coverage |
| --- | --- |
| `01_call_and_tool.py` | Real `Model.call`, `ToolCall`, and `Toolkit.execute` objects |
| `02_sync_async_stream.py` | Consumed real sync and async stream objects plus usage |
| `03_expected_error.py` | Exact provider 503 escaping the workflow boundary |
| `04_privacy.py` | Content capture disabled while model, usage, and status remain |
| `05_live_gateway.py` | Mirascope OpenAI provider through the Respan gateway |
Loading