quraite 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. quraite/__init__.py +4 -0
  2. quraite/adapters/__init__.py +1 -1
  3. quraite/adapters/agno_adapter.py +50 -92
  4. quraite/adapters/base.py +26 -76
  5. quraite/adapters/bedrock_agents_adapter.py +23 -76
  6. quraite/adapters/flowise_adapter.py +31 -72
  7. quraite/adapters/google_adk_adapter.py +28 -94
  8. quraite/adapters/http_adapter.py +28 -44
  9. quraite/adapters/langchain_adapter.py +51 -118
  10. quraite/adapters/langchain_server_adapter.py +37 -89
  11. quraite/adapters/langflow_adapter.py +15 -60
  12. quraite/adapters/n8n_adapter.py +19 -63
  13. quraite/adapters/openai_agents_adapter.py +35 -59
  14. quraite/adapters/pydantic_ai_adapter.py +27 -97
  15. quraite/adapters/smolagents_adapter.py +21 -82
  16. quraite/constants/framework.py +14 -0
  17. quraite/schema/__init__.py +4 -0
  18. quraite/schema/invoke.py +46 -0
  19. quraite/schema/message.py +20 -21
  20. quraite/serve/__init__.py +4 -0
  21. quraite/serve/cloudflared.py +3 -2
  22. quraite/serve/server.py +305 -0
  23. quraite/tracing/__init__.py +8 -5
  24. quraite/tracing/constants.py +0 -14
  25. quraite/tracing/setup.py +129 -0
  26. quraite/tracing/span_exporter.py +6 -6
  27. quraite/tracing/span_processor.py +6 -7
  28. quraite/tracing/tool_extractors.py +1 -1
  29. quraite/tracing/trace.py +36 -24
  30. quraite/utils/json_utils.py +2 -2
  31. {quraite-0.1.2.dist-info → quraite-0.1.4.dist-info}/METADATA +54 -62
  32. quraite-0.1.4.dist-info/RECORD +37 -0
  33. quraite/schema/response.py +0 -16
  34. quraite/serve/local_agent.py +0 -360
  35. quraite/traces/traces_adk_openinference.json +0 -379
  36. quraite/traces/traces_agno_multi_agent.json +0 -669
  37. quraite/traces/traces_agno_openinference.json +0 -321
  38. quraite/traces/traces_crewai_openinference.json +0 -155
  39. quraite/traces/traces_langgraph_openinference.json +0 -349
  40. quraite/traces/traces_langgraph_openinference_multi_agent.json +0 -2705
  41. quraite/traces/traces_langgraph_traceloop.json +0 -510
  42. quraite/traces/traces_openai_agents_multi_agent_1.json +0 -402
  43. quraite/traces/traces_openai_agents_openinference.json +0 -341
  44. quraite/traces/traces_pydantic_openinference.json +0 -286
  45. quraite/traces/traces_pydantic_openinference_multi_agent_1.json +0 -399
  46. quraite/traces/traces_pydantic_openinference_multi_agent_2.json +0 -398
  47. quraite/traces/traces_smol_agents_openinference.json +0 -397
  48. quraite/traces/traces_smol_agents_tool_calling_openinference.json +0 -704
  49. quraite-0.1.2.dist-info/RECORD +0 -49
  50. {quraite-0.1.2.dist-info → quraite-0.1.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,4 @@
1
+ from quraite.schema.invoke import InvokeInput, InvokeOutput
2
+ from quraite.schema.message import UserMessage
3
+
4
+ __all__ = ["UserMessage", "InvokeInput", "InvokeOutput"]
@@ -0,0 +1,46 @@
1
+ """Request and response models for agent invocation endpoints."""
2
+
3
+
4
+ from collections.abc import Sequence
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from quraite.schema.message import AgentMessage, UserMessage
9
+ from quraite.tracing.trace import AgentTrace
10
+
11
+
12
+ class InvokeInput(BaseModel):
13
+ """
14
+ Request model for agent invocation endpoints.
15
+
16
+ Attributes:
17
+ user_message: UserMessage object
18
+ session_id: Optional conversation thread identifier
19
+ """
20
+
21
+ user_message: UserMessage
22
+ session_id: str | None = None
23
+
24
+ def user_message_str(self) -> str:
25
+ """Extract text content from user message."""
26
+ if not self.user_message.content:
27
+ raise ValueError("User message has no content")
28
+
29
+ for content_item in self.user_message.content:
30
+ if content_item.type == "text" and content_item.text:
31
+ return content_item.text
32
+
33
+ raise ValueError("No text content found in user message")
34
+
35
+
36
+ class InvokeOutput(BaseModel):
37
+ """
38
+ Output model for agent invocation endpoints.
39
+
40
+ Attributes:
41
+ agent_trace: AgentTrace object representing agent trace
42
+ agent_trajectory: List of AgentMessage objects representing agent trajectory
43
+ """
44
+
45
+ agent_trace: AgentTrace | None = None
46
+ agent_trajectory: Sequence[AgentMessage] | None = None
quraite/schema/message.py CHANGED
@@ -1,4 +1,5 @@
1
- from typing import Any, List, Literal, Optional, TypeAlias, Union
1
+ from collections.abc import Sequence
2
+ from typing import Any, Literal, TypeAlias
2
3
 
3
4
  from pydantic import BaseModel, Field
4
5
 
@@ -15,18 +16,18 @@ class MessageContentReasoning(BaseModel):
15
16
 
16
17
  class UserMessage(BaseModel):
17
18
  role: Literal["user"] = "user"
18
- name: Optional[str] = None
19
- content: List[MessageContentText]
19
+ name: str | None = None
20
+ content: list[MessageContentText]
20
21
 
21
22
 
22
23
  class DeveloperMessage(BaseModel):
23
24
  role: Literal["developer"] = "developer"
24
- content: List[MessageContentText]
25
+ content: list[MessageContentText]
25
26
 
26
27
 
27
28
  class SystemMessage(BaseModel):
28
29
  role: Literal["system"] = "system"
29
- content: List[MessageContentText]
30
+ content: list[MessageContentText]
30
31
 
31
32
 
32
33
  class ToolCall(BaseModel):
@@ -58,34 +59,32 @@ class LatencyInfo(BaseModel):
58
59
  class AssistantMessageMetadata(BaseModel):
59
60
  """Structured metadata for assistant messages."""
60
61
 
61
- tokens: TokenInfo = Field(default_factory=TokenInfo)
62
- cost: CostInfo = Field(default_factory=CostInfo)
63
- latency: LatencyInfo = Field(default_factory=LatencyInfo)
64
- model_info: ModelInfo = Field(default_factory=ModelInfo)
62
+ tokens: TokenInfo | None = None
63
+ cost: CostInfo | None = None
64
+ latency: LatencyInfo | None = None
65
+ model_info: ModelInfo | None = None
65
66
 
66
67
 
67
68
  class ToolMessageMetadata(BaseModel):
68
69
  """Structured metadata for tool messages."""
69
70
 
70
- latency: LatencyInfo = Field(default_factory=LatencyInfo)
71
+ latency: LatencyInfo | None = None
71
72
 
72
73
 
73
74
  class AssistantMessage(BaseModel):
74
75
  role: Literal["assistant"] = "assistant"
75
- agent_name: Optional[str] = None
76
- content: Optional[List[Union[MessageContentText, MessageContentReasoning]]] = None
77
- tool_calls: Optional[List[ToolCall]] = None
78
- metadata: AssistantMessageMetadata = Field(default_factory=AssistantMessageMetadata)
76
+ agent_name: str | None = None
77
+ content: Sequence[MessageContentText | MessageContentReasoning] | None = None
78
+ tool_calls: list[ToolCall] | None = None
79
+ metadata: AssistantMessageMetadata | None = None
79
80
 
80
81
 
81
82
  class ToolMessage(BaseModel):
82
83
  role: Literal["tool"] = "tool"
83
- tool_name: Optional[str] = None
84
- tool_call_id: Optional[str] = None
85
- content: List[MessageContentText]
86
- metadata: ToolMessageMetadata = Field(default_factory=ToolMessageMetadata)
84
+ tool_name: str | None = None
85
+ tool_call_id: str | None = None
86
+ content: list[MessageContentText]
87
+ metadata: ToolMessageMetadata | None = None
87
88
 
88
89
 
89
- AgentMessage: TypeAlias = Union[
90
- UserMessage, DeveloperMessage, SystemMessage, AssistantMessage, ToolMessage
91
- ]
90
+ AgentMessage: TypeAlias = UserMessage | DeveloperMessage | SystemMessage | AssistantMessage | ToolMessage
quraite/serve/__init__.py CHANGED
@@ -1 +1,5 @@
1
+ """Quraite serving utilities."""
1
2
 
3
+ from quraite.serve.server import create_app, run_agent, setup_tunnel
4
+
5
+ __all__ = ["create_app", "run_agent", "setup_tunnel"]
@@ -105,8 +105,9 @@ def download_cloudflared(force: bool = False) -> Path:
105
105
  cloudflared_cmd = (
106
106
  "cloudflared.exe" if platform.system() == "windows" else "cloudflared"
107
107
  )
108
- if shutil.which(cloudflared_cmd) and not force:
109
- return Path(shutil.which(cloudflared_cmd))
108
+ cloudflared_in_path = shutil.which(cloudflared_cmd)
109
+ if cloudflared_in_path and not force:
110
+ return Path(cloudflared_in_path)
110
111
 
111
112
  cloudflared_path = get_cloudflared_path()
112
113
  system_key = get_system()
@@ -0,0 +1,305 @@
1
+ """Simplified agent runner API for Quraite."""
2
+
3
+ import asyncio
4
+ import os
5
+ from typing import Any, Literal
6
+
7
+ import httpx
8
+ import uvicorn
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from pydantic import BaseModel
12
+
13
+ from quraite.adapters.base import BaseAdapter
14
+ from quraite.logger import get_logger
15
+ from quraite.schema import InvokeInput, InvokeOutput
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class TunnelConfig(BaseModel):
21
+ tunnel_obj: Any
22
+ agent_url: str
23
+ tunnel_type: Literal["ngrok", "cloudflare", "none"]
24
+
25
+
26
+ def setup_tunnel(
27
+ port: int,
28
+ host: str = "0.0.0.0",
29
+ tunnel: Literal["ngrok", "cloudflare"] = "cloudflare",
30
+ ) -> TunnelConfig:
31
+ """
32
+ Setup tunnel connection.
33
+
34
+ Args:
35
+ port: Local port to expose
36
+ host: Local host address
37
+ tunnel: Tunnel type
38
+
39
+ Returns:
40
+ Tuple of (public_url, tunnel_object)
41
+ """
42
+ if tunnel == "ngrok":
43
+ try:
44
+ from pyngrok import ngrok
45
+ except ImportError as e:
46
+ raise ImportError(
47
+ "Failed to import pyngrok. Install with: pip install 'quraite[pyngrok]'"
48
+ ) from e
49
+
50
+ try:
51
+ ngrok_tunnel = ngrok.connect(port)
52
+ public_url = ngrok_tunnel.public_url
53
+ logger.info("Ngrok tunnel established: %s", public_url)
54
+ return TunnelConfig(
55
+ tunnel_obj=ngrok_tunnel, agent_url=public_url, tunnel_type="ngrok"
56
+ )
57
+ except Exception as e:
58
+ logger.error(
59
+ "Failed to create ngrok tunnel: %s. "
60
+ "Make sure ngrok is installed and authenticated: https://ngrok.com/download",
61
+ e,
62
+ )
63
+ raise
64
+
65
+ elif tunnel == "cloudflare":
66
+ from quraite.serve.cloudflared import connect
67
+
68
+ cloudflared_tunnel = connect(
69
+ port, host=host if host != "0.0.0.0" else "localhost"
70
+ )
71
+ public_url = cloudflared_tunnel.public_url
72
+ logger.info("Cloudflare tunnel established: %s", public_url)
73
+ return TunnelConfig(
74
+ tunnel_obj=cloudflared_tunnel,
75
+ agent_url=public_url,
76
+ tunnel_type="cloudflare",
77
+ )
78
+
79
+
80
+ def _cleanup_tunnel(tunnel_obj: Any, tunnel_type: Literal["ngrok", "cloudflare", "none"]) -> None:
81
+ """Clean up tunnel connection."""
82
+ try:
83
+ if tunnel_type == "ngrok":
84
+ from pyngrok import ngrok
85
+
86
+ ngrok.disconnect(tunnel_obj.public_url)
87
+ ngrok.kill()
88
+ logger.info("Ngrok tunnel closed")
89
+ elif tunnel_type == "cloudflare":
90
+ if hasattr(tunnel_obj, "disconnect"):
91
+ tunnel_obj.disconnect()
92
+ elif hasattr(tunnel_obj, "stop"):
93
+ tunnel_obj.stop()
94
+ elif hasattr(tunnel_obj, "close"):
95
+ tunnel_obj.close()
96
+ logger.info("Cloudflare tunnel closed")
97
+ except Exception as e:
98
+ logger.warning("Error closing %s tunnel: %s", tunnel_type, e)
99
+
100
+
101
+ async def _update_backend_agent_url(
102
+ agent_id: str,
103
+ agent_url: str | None,
104
+ quraite_endpoint: str = "https://api.quraite.ai",
105
+ ) -> None:
106
+ """
107
+ Update backend with agent URL.
108
+
109
+ Args:
110
+ agent_id: Quraite platform agent ID
111
+ agent_url: Agent base URL
112
+ quraite_endpoint: Quraite API endpoint
113
+ """
114
+ if not agent_id or not agent_url:
115
+ return
116
+
117
+ endpoint = f"{quraite_endpoint.rstrip('/')}/agents/{agent_id}/config/url"
118
+ full_url = f"{agent_url.rstrip('/')}/v1/agents/completions"
119
+ payload = {"config": {"url": full_url}}
120
+
121
+ try:
122
+ async with httpx.AsyncClient(timeout=10.0) as client:
123
+ response = await client.patch(endpoint, json=payload)
124
+ response.raise_for_status()
125
+ logger.info("Agent URL registered with Quraite platform: %s", full_url)
126
+ except httpx.HTTPStatusError as e:
127
+ logger.warning(
128
+ "Failed to update agent URL in Quraite platform: HTTP %s - %s. Update manually with URL: %s",
129
+ e.response.status_code,
130
+ e.response.text,
131
+ full_url,
132
+ )
133
+ except httpx.RequestError as e:
134
+ logger.warning(
135
+ "Failed to connect to Quraite backend at %s: %s",
136
+ quraite_endpoint,
137
+ e,
138
+ )
139
+ except Exception as e:
140
+ logger.warning("Unexpected error updating agent URL: %s", e)
141
+
142
+
143
+ def create_app(
144
+ agent_adapter: BaseAdapter,
145
+ *,
146
+ agent_id: str | None = None,
147
+ tunnel_config: TunnelConfig | None = None,
148
+ ) -> FastAPI:
149
+ """
150
+ Create a standalone FastAPI app for an agent.
151
+
152
+ Returns a FastAPI application that developers can run with any ASGI server
153
+ (uvicorn, hypercorn, etc.). The app is self-contained with lifespan management.
154
+
155
+ Args:
156
+ agent_adapter: The agent adapter instance (must inherit from BaseAdapter)
157
+ agent_id: Optional Quraite platform agent ID for backend registration
158
+ tunnel_config: Optional TunnelConfig object. This is required if you are using a tunnel. To get the tunnel config, use the setup_tunnel function.
159
+
160
+ Returns:
161
+ FastAPI application instance with agent endpoints
162
+ """
163
+ logger.info("Creating FastAPI app with agent adapter")
164
+
165
+ adapter = agent_adapter
166
+
167
+ # Extract tunnel info
168
+ tunnel_obj = tunnel_config.tunnel_obj if tunnel_config else None
169
+ agent_url = tunnel_config.agent_url if tunnel_config else None
170
+ tunnel_type = tunnel_config.tunnel_type if tunnel_config else None
171
+
172
+ # Create lifespan with tunnel and backend registration
173
+ from contextlib import asynccontextmanager
174
+
175
+ @asynccontextmanager
176
+ async def lifespan(app: FastAPI):
177
+ # Startup
178
+ logger.info("Agent server started successfully")
179
+ path = "/v1/agents/completions"
180
+
181
+ # Register with Quraite backend if agent_id and tunnel provided
182
+ if agent_id and tunnel_type:
183
+ quraite_endpoint = os.getenv("QURAITE_ENDPOINT", "https://api.quraite.ai")
184
+ await _update_backend_agent_url(agent_id, agent_url, quraite_endpoint)
185
+
186
+ if tunnel_type:
187
+ logger.info(f"Agent publicly available at {agent_url}{path}")
188
+ if not agent_id:
189
+ logger.info(
190
+ f"Add this URL to your agent in the Quraite platform: {agent_url}{path}"
191
+ )
192
+ elif agent_url:
193
+ logger.info(
194
+ f"Agent running locally at {agent_url}{path}. Use tunnel option to make it publicly available."
195
+ )
196
+
197
+ yield
198
+
199
+ # Shutdown - cleanup tunnel
200
+ if tunnel_obj and tunnel_type:
201
+ logger.info("Closing %s tunnel...", tunnel_type)
202
+ loop = asyncio.get_event_loop()
203
+ await loop.run_in_executor(None, _cleanup_tunnel, tunnel_obj, tunnel_type)
204
+
205
+ # Create FastAPI app with lifespan
206
+ app = FastAPI(title="Quraite Agent Server", lifespan=lifespan)
207
+
208
+ # Add CORS middleware
209
+ app.add_middleware(
210
+ CORSMiddleware,
211
+ allow_origins=["*"],
212
+ allow_credentials=True,
213
+ allow_methods=["*"],
214
+ allow_headers=["*"],
215
+ )
216
+
217
+ # Health check endpoint
218
+ @app.get("/")
219
+ def health_check():
220
+ """Health check endpoint."""
221
+ return {
222
+ "status": "ok",
223
+ "message": "Agent server is running",
224
+ }
225
+
226
+ # Agent invocation endpoint
227
+ @app.post("/v1/agents/completions", response_model=InvokeOutput)
228
+ async def invoke_agent(request: InvokeInput) -> InvokeOutput:
229
+ """
230
+ Agent invocation endpoint.
231
+
232
+ Args:
233
+ request: InvokeInput containing user_message and session_id
234
+
235
+ Returns:
236
+ InvokeOutput with agent response
237
+
238
+ Raises:
239
+ HTTPException: If agent invocation fails
240
+ """
241
+ try:
242
+ response = await adapter.ainvoke(input=request)
243
+ return response
244
+ except Exception as e:
245
+ logger.exception("Agent invocation failed")
246
+ raise HTTPException(
247
+ status_code=500,
248
+ detail=f"Agent invocation failed: {str(e)}",
249
+ ) from e
250
+
251
+ logger.info("FastAPI app created successfully")
252
+ return app
253
+
254
+
255
+ def run_agent(
256
+ agent_adapter: BaseAdapter,
257
+ *,
258
+ agent_id: str | None = None,
259
+ port: int = 8080,
260
+ host: str = "0.0.0.0",
261
+ tunnel: Literal["none", "cloudflare", "ngrok"] = "none",
262
+ ) -> None:
263
+ """
264
+ Run an agent with a single line of code.
265
+
266
+ This function starts a FastAPI server for the provided agent adapter,
267
+ providing the best developer experience.
268
+
269
+ Args:
270
+ agent_adapter: The agent adapter instance (must inherit from BaseAdapter)
271
+ agent_id: Optional Quraite platform agent ID
272
+ port: Port number for the local server (default: 8080)
273
+ host: Host address (default: "0.0.0.0")
274
+ tunnel: Tunnel type ("none", "cloudflare", "ngrok") for public access
275
+
276
+ Example:
277
+ ```python
278
+ from quraite import run_agent
279
+ from quraite.adapters.langchain_adapter import LangchainAdapter
280
+
281
+ # Create adapter with tracing enabled
282
+ adapter = LangchainAdapter(agent_graph=my_graph, tracing=True)
283
+
284
+ # Run with tunnel
285
+ run_agent(adapter, tunnel="cloudflare", agent_id="agent-123")
286
+ ```
287
+ """
288
+ logger.info(
289
+ "Starting agent runner (port=%d, tunnel=%s, agent_id=%s)",
290
+ port,
291
+ tunnel,
292
+ agent_id,
293
+ )
294
+
295
+ # Setup tunnel if requested
296
+ tunnel_config = None
297
+ if tunnel != "none":
298
+ logger.info("Setting up %s tunnel on port %s...", tunnel, port)
299
+ tunnel_config = setup_tunnel(port, host, tunnel)
300
+
301
+ # Create app with tunnel config
302
+ app = create_app(agent_adapter, agent_id=agent_id, tunnel_config=tunnel_config)
303
+
304
+ # Run server
305
+ uvicorn.run(app, host=host, port=port)
@@ -1,8 +1,10 @@
1
1
  """Tracing infrastructure for OpenTelemetry span collection and processing."""
2
2
 
3
- from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
4
- from quraite.tracing.span_processor import QuraiteSimpleSpanProcessor
5
- from quraite.tracing.tool_extractors import Framework, ToolCallInfo, get_tool_extractor
3
+ from quraite.constants.framework import Framework
4
+ from quraite.tracing.setup import setup_tracing
5
+ from quraite.tracing.span_exporter import QuraiteSpanExporter
6
+ from quraite.tracing.span_processor import QuraiteSpanProcessor
7
+ from quraite.tracing.tool_extractors import ToolCallInfo, get_tool_extractor
6
8
  from quraite.tracing.trace import AgentSpan, AgentTrace, CostInfo, TokenInfo
7
9
  from quraite.tracing.types import Event, Link, Resource, SpanContext, Status
8
10
 
@@ -15,11 +17,12 @@ __all__ = [
15
17
  "Tool",
16
18
  "ToolCallInfo",
17
19
  "get_tool_extractor",
18
- "QuraiteInMemorySpanExporter",
19
- "QuraiteSimpleSpanProcessor",
20
+ "QuraiteSpanExporter",
21
+ "QuraiteSpanProcessor",
20
22
  "Event",
21
23
  "Link",
22
24
  "Resource",
23
25
  "SpanContext",
24
26
  "Status",
27
+ "setup_tracing",
25
28
  ]
@@ -1,15 +1 @@
1
- from enum import Enum
2
-
3
1
  QURAITE_TRACER_NAME = "quraite.instrumentation"
4
-
5
-
6
- class Framework(str, Enum):
7
- """Supported agent frameworks."""
8
-
9
- DEFAULT = "default"
10
- PYDANTIC_AI = "pydantic_ai"
11
- LANGCHAIN = "langchain"
12
- GOOGLE_ADK = "google_adk"
13
- OPENAI_AGENTS = "openai_agents"
14
- AGNO = "agno"
15
- SMOLAGENTS = "smolagents"
@@ -0,0 +1,129 @@
1
+ """Centralized tracing setup and framework instrumentation."""
2
+
3
+
4
+ from opentelemetry import trace
5
+ from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
6
+
7
+ from quraite.constants.framework import Framework
8
+ from quraite.logger import get_logger
9
+ from quraite.tracing.span_exporter import QuraiteSpanExporter
10
+ from quraite.tracing.span_processor import QuraiteSpanProcessor
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def setup_tracing(
16
+ frameworks: list[Framework | str] = [],
17
+ tracer_provider: SDKTracerProvider | None = None,
18
+ ) -> SDKTracerProvider:
19
+ """
20
+ Configure tracing once at app startup.
21
+
22
+ Args:
23
+ frameworks: Frameworks to instrument (e.g., [Framework.LANGCHAIN])
24
+ tracer_provider: Optional existing provider (auto-creates if None)
25
+
26
+ Returns:
27
+ Configured TracerProvider to pass to adapters
28
+ """
29
+ provider = tracer_provider or _create_tracer_provider()
30
+ _inject_quraite_processor(provider)
31
+
32
+ for framework in frameworks:
33
+ _instrument_framework(framework, provider)
34
+
35
+ logger.info("Tracing setup complete for frameworks: %s", frameworks)
36
+ return provider
37
+
38
+
39
+ def _create_tracer_provider() -> SDKTracerProvider:
40
+ """Create TracerProvider or use existing global provider."""
41
+ global_provider = trace.get_tracer_provider()
42
+ if isinstance(global_provider, SDKTracerProvider):
43
+ logger.debug("Using existing global TracerProvider")
44
+ return global_provider
45
+
46
+ logger.debug("Creating new TracerProvider")
47
+ return SDKTracerProvider()
48
+
49
+
50
+ def _inject_quraite_processor(provider: SDKTracerProvider) -> None:
51
+ """Inject QuraiteSpanProcessor if not already present."""
52
+ has_quraite_processor = any(
53
+ isinstance(processor, QuraiteSpanProcessor)
54
+ for processor in provider._active_span_processor._span_processors
55
+ )
56
+
57
+ if not has_quraite_processor:
58
+ logger.debug("Injecting QuraiteSpanProcessor")
59
+ exporter = QuraiteSpanExporter()
60
+ processor = QuraiteSpanProcessor(exporter)
61
+ provider.add_span_processor(processor)
62
+ logger.info("QuraiteSpanProcessor injected")
63
+ else:
64
+ logger.debug("QuraiteSpanProcessor already present")
65
+
66
+
67
+ def _instrument_framework(framework: Framework | str, provider: SDKTracerProvider) -> None:
68
+ """Instrument a specific framework (idempotent)."""
69
+ framework_str = framework.value if isinstance(framework, Framework) else framework
70
+
71
+ try:
72
+ match framework_str:
73
+ case Framework.LANGCHAIN | "langchain":
74
+ from openinference.instrumentation.langchain import (
75
+ LangChainInstrumentor,
76
+ )
77
+
78
+ LangChainInstrumentor().instrument(tracer_provider=provider)
79
+
80
+ case Framework.AGNO | "agno":
81
+ from openinference.instrumentation.agno import AgnoInstrumentor
82
+
83
+ AgnoInstrumentor().instrument(tracer_provider=provider)
84
+
85
+ case Framework.PYDANTIC_AI | "pydantic_ai":
86
+ from openinference.instrumentation.pydantic_ai import (
87
+ OpenInferenceSpanProcessor,
88
+ )
89
+
90
+ # Pydantic AI instrumentation requires the tracer provider to be set globally
91
+ trace.set_tracer_provider(provider)
92
+ provider.add_span_processor(OpenInferenceSpanProcessor())
93
+
94
+ case Framework.GOOGLE_ADK | "google_adk":
95
+ from openinference.instrumentation.google_adk import (
96
+ GoogleADKInstrumentor,
97
+ )
98
+
99
+ GoogleADKInstrumentor().instrument(tracer_provider=provider)
100
+
101
+ case Framework.SMOLAGENTS | "smolagents":
102
+ from openinference.instrumentation.smolagents import (
103
+ SmolagentsInstrumentor,
104
+ )
105
+
106
+ SmolagentsInstrumentor().instrument(tracer_provider=provider)
107
+
108
+ case Framework.OPENAI_AGENTS | "openai_agents":
109
+ from openinference.instrumentation.openai_agents import (
110
+ OpenAIAgentsInstrumentor,
111
+ )
112
+
113
+ OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)
114
+ case Framework.OPENAI | "openai":
115
+ from openinference.instrumentation.openai import OpenAIInstrumentor
116
+
117
+ OpenAIInstrumentor().instrument(tracer_provider=provider)
118
+ case _:
119
+ logger.warning("Unknown framework: %s", framework_str)
120
+ return
121
+
122
+ logger.info("Instrumented framework: %s", framework_str)
123
+
124
+ except ImportError as e:
125
+ logger.warning(
126
+ "Failed to import instrumentor for %s: %s. Install the instrumentation package.",
127
+ framework_str,
128
+ e,
129
+ )
@@ -8,10 +8,10 @@ from opentelemetry.sdk.trace import ReadableSpan
8
8
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
9
9
 
10
10
 
11
- class QuraiteInMemorySpanExporter(SpanExporter):
11
+ class QuraiteSpanExporter(SpanExporter):
12
12
  def __init__(self) -> None:
13
13
  # self.spans: typing.List[ReadableSpan] = []
14
- self.traces: typing.Dict[int, typing.List[ReadableSpan]] = defaultdict(list)
14
+ self.traces: dict[int, list[ReadableSpan]] = defaultdict(list)
15
15
  self._stopped = False
16
16
  self._lock = threading.Lock()
17
17
 
@@ -28,7 +28,7 @@ class QuraiteInMemorySpanExporter(SpanExporter):
28
28
 
29
29
  return SpanExportResult.SUCCESS
30
30
 
31
- def get_spans_by_trace_id(self, trace_id: int) -> typing.List[ReadableSpan]:
31
+ def get_spans_by_trace_id(self, trace_id: int) -> list[ReadableSpan]:
32
32
  """Get all spans for a specific trace ID"""
33
33
  return self.traces.get(trace_id, [])
34
34
 
@@ -43,11 +43,11 @@ class QuraiteInMemorySpanExporter(SpanExporter):
43
43
  def force_flush(self, timeout_millis: int = 30000) -> bool:
44
44
  return True
45
45
 
46
- def get_traces(self) -> typing.Dict[int, typing.List[ReadableSpan]]:
46
+ def get_traces(self) -> dict[int, list[ReadableSpan]]:
47
47
  """Get all spans grouped by trace ID"""
48
48
  return dict(self.traces)
49
49
 
50
- def get_trace(self, trace_id: int) -> typing.List[ReadableSpan]:
50
+ def get_trace(self, trace_id: int) -> list[ReadableSpan]:
51
51
  """Get all spans for a specific trace"""
52
52
  return self.traces.get(trace_id, [])
53
53
 
@@ -85,7 +85,7 @@ class QuraiteInMemorySpanExporter(SpanExporter):
85
85
  # print(f" {indent} └─ Attributes: {dict(span.attributes)}")
86
86
  # print()
87
87
 
88
- def save_traces_to_file(self, filename: typing.Optional[str] = "traces.json"):
88
+ def save_traces_to_file(self, filename: str | None = "traces.json"):
89
89
  """Save a trace to a file"""
90
90
  traces = []
91
91
  for trace_id, spans in self.traces.items():