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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ MAIN_MODEL_PROVIDER=openai
MAIN_MODEL=gpt-4o
FAST_MODEL_PROVIDER=openai
FAST_MODEL=gpt-4o-mini

# Optional Monocle telemetry (requires: pip install deep-researcher[monocle])
# MONOCLE_TRACING=true
# MONOCLE_EXPORTERS=file # file, console, okahu, s3, blob, gcs (default: file)
# OKAHU_API_KEY=okh_xxxxxxxx # required only for the `okahu` exporter
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@ LLMs are configured and managed in the `deep_researcher/llm_config.py` file.

The Deep Research assistant integrates with OpenAI's trace monitoring system. Each research session generates a trace ID that can be used to monitor the execution flow and agent interactions in real-time through the OpenAI platform.

### Monocle Tracing

This project also supports [Monocle](https://github.com/monocle2ai/monocle), an OpenTelemetry-based tracer for agentic applications. It records each run end-to-end: LLM calls, agent steps, and tool invocations, with inputs, outputs, timings, and token counts.

Monocle is an optional extra. Install it and add the following to your `.env` file:

```bash
pip install "deep-researcher[monocle]"
```

```bash
MONOCLE_TRACING=true
MONOCLE_EXPORTERS=file # file, console, okahu, s3, blob, gcs (default: file)
OKAHU_API_KEY=okh_xxxxxxxx # required only for the `okahu` exporter
```

Each run writes one trace file to `.monocle/`; open it in the [Monocle VS Code extension](https://marketplace.visualstudio.com/items?itemName=OkahuAI.monocle-apptrace). Connect to [Okahu](https://www.okahu.ai) to analyze traces across runs (via the `okahu` exporter).

## Observations and Limitations

### Rate Limits
Expand Down
66 changes: 46 additions & 20 deletions deep_researcher/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import asyncio
import argparse
from contextlib import AsyncExitStack
from .iterative_research import IterativeResearcher
from .deep_research import DeepResearcher
from typing import Literal
from dotenv import load_dotenv
from .utils.telemetry import setup_telemetry, trace_span, trace_scope, current_span

load_dotenv(override=True)

# Optional Monocle telemetry: a no-op unless MONOCLE_TRACING is truthy AND the
# optional monocle_apptrace package is installed (pip install deep-researcher[monocle]).
setup_telemetry(workflow_name="openai-agents-deep-research")


async def main() -> None:
parser = argparse.ArgumentParser(description="Deep Research Assistant")
Expand Down Expand Up @@ -34,26 +40,46 @@ async def main() -> None:
print(f"Starting deep research on: {query}")
print(f"Max iterations: {args.max_iterations}, Max time: {args.max_time} minutes")

if args.model == "deep":
manager = DeepResearcher(
max_iterations=args.max_iterations,
max_time_minutes=args.max_time,
verbose=args.verbose,
tracing=args.tracing
)
report = await manager.run(query)
else:
manager = IterativeResearcher(
max_iterations=args.max_iterations,
max_time_minutes=args.max_time,
verbose=args.verbose,
tracing=args.tracing
)
report = await manager.run(
query,
output_length=args.output_length,
output_instructions=args.output_instructions
)
# Under Monocle we shape the whole run as ONE trace, per the recipe:
# workflow -> agentic.turn (this user request) -> agentic.invocation (each agent).
# The orchestrator fires many independent Runner.run calls; without an enclosing
# span each starts its own trace. A single workflow root + one agentic.turn here
# collapses each Runner.run into that turn as an invocation. All helpers are
# no-ops when telemetry is disabled/absent, so the run is unchanged.
report = ""
async with AsyncExitStack() as stack:
await stack.enter_async_context(trace_scope("agentic.session"))
await stack.enter_async_context(trace_span(span_name="openai-agents-deep-research"))
await stack.enter_async_context(trace_scope("agentic.turn"))
await stack.enter_async_context(trace_span(
span_name="research_turn",
attributes={"span.type": "agentic.turn", "entity.1.name": query[:128]},
events=[{"name": "data.input", "attributes": {"input": query}}],
))
if args.model == "deep":
manager = DeepResearcher(
max_iterations=args.max_iterations,
max_time_minutes=args.max_time,
verbose=args.verbose,
tracing=args.tracing
)
report = await manager.run(query)
else:
manager = IterativeResearcher(
max_iterations=args.max_iterations,
max_time_minutes=args.max_time,
verbose=args.verbose,
tracing=args.tracing
)
report = await manager.run(
query,
output_length=args.output_length,
output_instructions=args.output_instructions
)
# record the turn's final output on the agentic.turn span before it closes
turn_span = current_span()
if turn_span is not None:
turn_span.add_event("data.output", {"response": report})

print("\n=== Final Report ===")
print(report)
Expand Down
110 changes: 110 additions & 0 deletions deep_researcher/utils/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Optional Monocle telemetry integration.

Monocle (``monocle_apptrace``) is an optional observability dependency, activated
only when ``MONOCLE_TRACING`` is truthy AND the package is installed
(``pip install deep-researcher[monocle]``). The env interface is deliberately
unprefixed (``MONOCLE_TRACING`` / ``MONOCLE_EXPORTERS`` / ``OKAHU_API_KEY``) so it
matches the surface used across the other Monocle-enabled apps.

When telemetry is disabled or absent, every helper here is a clean no-op: no
import errors, no extra spans, no behaviour change.
"""

import contextlib
import os
from typing import Any, Optional

_TRUTHY = {"1", "true", "yes", "on"}

# Mirror of monocle_apptrace's supported exporters, kept local so a typo fails
# fast with a clear message instead of an opaque upstream error.
_MONOCLE_EXPORTERS = ("file", "console", "okahu", "s3", "blob", "gcs")

# Set True by setup_telemetry only when Monocle is both enabled and installed.
_ACTIVE = False


def monocle_enabled() -> bool:
"""Return True if Monocle telemetry is enabled via MONOCLE_TRACING."""
value = os.environ.get("MONOCLE_TRACING", "")
return value.strip().lower() in _TRUTHY


def _exporters() -> str:
"""The configured, comma-separated exporter string (default ``file``)."""
value = os.environ.get("MONOCLE_EXPORTERS", "")
return value.strip() if value and value.strip() else "file"


def setup_telemetry(workflow_name: str) -> bool:
"""Initialise Monocle telemetry when enabled; a clean no-op otherwise.

Reads MONOCLE_EXPORTERS, validates it, then forwards the comma-separated
string to setup_monocle_telemetry. Returns True when telemetry was activated.
"""
global _ACTIVE
if not monocle_enabled():
return False

exporters = _exporters()
selected = [e.strip() for e in exporters.split(",") if e.strip()]

# Fail fast on an unknown exporter or okahu without a key, before instrumenting.
unknown = [e for e in selected if e not in _MONOCLE_EXPORTERS]
if unknown:
raise ValueError(
f"MONOCLE_EXPORTERS has unknown exporter(s): {', '.join(unknown)}. "
f"Allowed: {', '.join(_MONOCLE_EXPORTERS)}."
)
if "okahu" in selected and not os.environ.get("OKAHU_API_KEY"):
raise ValueError("Monocle 'okahu' exporter is selected but OKAHU_API_KEY is not set.")

try:
from monocle_apptrace import setup_monocle_telemetry
except ImportError as exc:
raise RuntimeError(
"MONOCLE_TRACING is enabled but monocle_apptrace is not installed. "
"Install the 'monocle' extra: pip install \"deep-researcher[monocle]\"."
) from exc

# monocle_exporters_list takes the comma-separated string as-is (monocle's API).
setup_monocle_telemetry(workflow_name=workflow_name, monocle_exporters_list=exporters)
_ACTIVE = True
return True


@contextlib.asynccontextmanager
async def trace_span(*args: Any, **kwargs: Any):
"""Open a Monocle span when telemetry is active; a no-op otherwise."""
if _ACTIVE:
from monocle_apptrace.instrumentation.common.instrumentor import amonocle_trace

async with amonocle_trace(*args, **kwargs):
yield
else:
yield


@contextlib.asynccontextmanager
async def trace_scope(*args: Any, **kwargs: Any):
"""Open a Monocle scope when telemetry is active; a no-op otherwise."""
if _ACTIVE:
from monocle_apptrace.instrumentation.common.scope_wrapper import (
amonocle_trace_scope,
)

async with amonocle_trace_scope(*args, **kwargs):
yield
else:
yield


def current_span() -> Optional[Any]:
"""Return the current Monocle span when telemetry is active, else None."""
if _ACTIVE:
from monocle_apptrace.instrumentation.common.wrapper import (
get_current_monocle_span,
)

return get_current_monocle_span()
return None
6 changes: 6 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
'dev': [
'pytest',
'pytest-asyncio'
],
# Optional agent observability via Monocle. Install with:
# pip install deep-researcher[monocle]
# and enable at runtime with MONOCLE_TRACING=true (see README).
'monocle': [
'monocle_apptrace'
]
},
entry_points={
Expand Down