quraite 0.1.0__py3-none-any.whl → 0.1.2__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 (49) hide show
  1. quraite/__init__.py +3 -3
  2. quraite/adapters/__init__.py +134 -134
  3. quraite/adapters/agno_adapter.py +157 -159
  4. quraite/adapters/base.py +123 -123
  5. quraite/adapters/bedrock_agents_adapter.py +343 -343
  6. quraite/adapters/flowise_adapter.py +275 -275
  7. quraite/adapters/google_adk_adapter.py +211 -209
  8. quraite/adapters/http_adapter.py +255 -239
  9. quraite/adapters/{langgraph_adapter.py → langchain_adapter.py} +305 -304
  10. quraite/adapters/{langgraph_server_adapter.py → langchain_server_adapter.py} +252 -252
  11. quraite/adapters/langflow_adapter.py +192 -192
  12. quraite/adapters/n8n_adapter.py +220 -220
  13. quraite/adapters/openai_agents_adapter.py +267 -269
  14. quraite/adapters/pydantic_ai_adapter.py +307 -312
  15. quraite/adapters/smolagents_adapter.py +148 -152
  16. quraite/logger.py +61 -61
  17. quraite/schema/message.py +91 -91
  18. quraite/schema/response.py +16 -16
  19. quraite/serve/__init__.py +1 -1
  20. quraite/serve/cloudflared.py +210 -210
  21. quraite/serve/local_agent.py +360 -360
  22. quraite/traces/traces_adk_openinference.json +379 -0
  23. quraite/traces/traces_agno_multi_agent.json +669 -0
  24. quraite/traces/traces_agno_openinference.json +321 -0
  25. quraite/traces/traces_crewai_openinference.json +155 -0
  26. quraite/traces/traces_langgraph_openinference.json +349 -0
  27. quraite/traces/traces_langgraph_openinference_multi_agent.json +2705 -0
  28. quraite/traces/traces_langgraph_traceloop.json +510 -0
  29. quraite/traces/traces_openai_agents_multi_agent_1.json +402 -0
  30. quraite/traces/traces_openai_agents_openinference.json +341 -0
  31. quraite/traces/traces_pydantic_openinference.json +286 -0
  32. quraite/traces/traces_pydantic_openinference_multi_agent_1.json +399 -0
  33. quraite/traces/traces_pydantic_openinference_multi_agent_2.json +398 -0
  34. quraite/traces/traces_smol_agents_openinference.json +397 -0
  35. quraite/traces/traces_smol_agents_tool_calling_openinference.json +704 -0
  36. quraite/tracing/__init__.py +25 -24
  37. quraite/tracing/constants.py +15 -16
  38. quraite/tracing/span_exporter.py +101 -115
  39. quraite/tracing/span_processor.py +47 -49
  40. quraite/tracing/tool_extractors.py +309 -290
  41. quraite/tracing/trace.py +564 -564
  42. quraite/tracing/types.py +179 -179
  43. quraite/tracing/utils.py +170 -170
  44. quraite/utils/json_utils.py +269 -269
  45. quraite-0.1.2.dist-info/METADATA +386 -0
  46. quraite-0.1.2.dist-info/RECORD +49 -0
  47. {quraite-0.1.0.dist-info → quraite-0.1.2.dist-info}/WHEEL +1 -1
  48. quraite-0.1.0.dist-info/METADATA +0 -44
  49. quraite-0.1.0.dist-info/RECORD +0 -35
@@ -1,290 +1,309 @@
1
- """
2
- Framework-specific tool extractors for converting span attributes to standardized tool call information.
3
-
4
- These extractors handle the varying attribute structures across different agent frameworks
5
- (pydantic, langgraph, adk, openai_agents, agno, smolagents, etc.)
6
- """
7
-
8
- import json
9
- from typing import Any, Protocol
10
-
11
- from quraite.tracing.constants import Framework
12
-
13
-
14
- class ToolCallInfo:
15
- """Standardized tool call information extracted from a TOOL span."""
16
-
17
- def __init__(
18
- self,
19
- tool_name: str,
20
- tool_call_id: str | None,
21
- arguments: str | dict,
22
- response: Any,
23
- ):
24
- self.tool_name = tool_name
25
- self.tool_call_id = tool_call_id
26
- self.arguments = arguments
27
- self.response = response
28
-
29
- def to_dict(self) -> dict[str, Any]:
30
- return {
31
- "role": "tool",
32
- "tool_name": self.tool_name,
33
- "tool_call_id": self.tool_call_id,
34
- "arguments": self.arguments,
35
- "response": self.response,
36
- }
37
-
38
-
39
- class ToolExtractor(Protocol):
40
- """Protocol for framework-specific tool extractors."""
41
-
42
- def __call__(self, span: dict[str, Any]) -> ToolCallInfo | None: ...
43
-
44
-
45
- # =============================================================================
46
- # Framework-specific tool extractors
47
- # =============================================================================
48
-
49
-
50
- def extract_tool_pydantic(span: dict[str, Any]) -> ToolCallInfo | None:
51
- """
52
- Extract tool info from Pydantic AI tool spans.
53
-
54
- Attributes:
55
- - tool.name: "customer_balance"
56
- - tool_call.id: "call_xxx"
57
- - tool_arguments: "{\"include_pending\":true}"
58
- - tool_response: "$123.45"
59
- """
60
- attrs = span.get("attributes", {})
61
-
62
- tool_name = attrs.get("tool.name") or attrs.get("gen_ai.tool.name")
63
- if not tool_name:
64
- return None
65
-
66
- tool_call_id = attrs.get("tool_call.id") or attrs.get("gen_ai.tool.call.id")
67
- arguments = attrs.get("tool_arguments", "{}")
68
- response = attrs.get("tool_response", "")
69
-
70
- return ToolCallInfo(
71
- tool_name=tool_name,
72
- tool_call_id=tool_call_id,
73
- arguments=arguments,
74
- response=response,
75
- )
76
-
77
-
78
- def extract_tool_langgraph(span: dict[str, Any]) -> ToolCallInfo | None:
79
- """
80
- Extract tool info from LangGraph tool spans.
81
-
82
- Attributes:
83
- - tool.name: "add"
84
- - tool.description: "Add two numbers."
85
- - input.value: "{'b': 1, 'a': 1}"
86
- - output.value: JSON with content
87
- """
88
- attrs = span.get("attributes", {})
89
-
90
- tool_name = attrs.get("tool.name")
91
- if not tool_name:
92
- return None
93
-
94
- arguments = attrs.get("input.value", "{}")
95
- output_value = attrs.get("output.value", "")
96
-
97
- # Also check for response attribute (some LangGraph spans store response here)
98
- response_value = attrs.get("response", output_value)
99
-
100
- # Try to parse output to extract content
101
- response = response_value
102
- if isinstance(response_value, str):
103
- try:
104
- parsed = json.loads(response_value)
105
- if isinstance(parsed, dict):
106
- # Check if response field contains JSON string (nested JSON)
107
- if "response" in parsed and isinstance(parsed["response"], str):
108
- try:
109
- inner_parsed = json.loads(parsed["response"])
110
- if isinstance(inner_parsed, dict) and "update" in inner_parsed:
111
- parsed = inner_parsed
112
- except (json.JSONDecodeError, TypeError):
113
- pass
114
-
115
- # First check for direct content field
116
- if "content" in parsed:
117
- response = parsed.get("content", response_value)
118
- # Check for update.messages structure (LangGraph graph updates)
119
- # this comes when you use supervisor agent with multiple agents
120
- elif "update" in parsed:
121
- update = parsed.get("update", {})
122
- messages = update.get("messages", [])
123
- # Find the last tool message
124
- for msg in reversed(messages):
125
- if isinstance(msg, dict) and msg.get("type") == "tool":
126
- content = msg.get("content", "")
127
- if content:
128
- response = content
129
- break
130
- else:
131
- # No tool message found, keep original response
132
- response = response_value
133
- else:
134
- response = response_value
135
- except json.JSONDecodeError:
136
- pass
137
-
138
- return ToolCallInfo(
139
- tool_name=tool_name,
140
- tool_call_id=None, # LangGraph doesn't always have call IDs in tool spans
141
- arguments=arguments,
142
- response=response,
143
- )
144
-
145
-
146
- def extract_tool_adk(span: dict[str, Any]) -> ToolCallInfo | None:
147
- """
148
- Extract tool info from Google ADK tool spans.
149
-
150
- Attributes:
151
- - tool.name: "get_weather"
152
- - tool.parameters: "{\"city\": \"New York\"}"
153
- - gcp.vertex.agent.tool_call_args: "{\"city\": \"New York\"}"
154
- - gcp.vertex.agent.tool_response: JSON response
155
- - output.value: JSON with id, name, response
156
- """
157
- attrs = span.get("attributes", {})
158
-
159
- tool_name = attrs.get("tool.name") or attrs.get("gen_ai.tool.name")
160
- if not tool_name:
161
- return None
162
-
163
- # Skip merged tool spans
164
- if tool_name == "(merged tools)":
165
- return None
166
-
167
- tool_call_id = attrs.get("gen_ai.tool.call.id")
168
- arguments = (
169
- attrs.get("tool.parameters")
170
- or attrs.get("gcp.vertex.agent.tool_call_args")
171
- or attrs.get("input.value", "{}")
172
- )
173
-
174
- # Get response from various possible locations
175
- response = attrs.get("gcp.vertex.agent.tool_response", "")
176
- if not response or response == "<not serializable>":
177
- output_value = attrs.get("output.value", "")
178
- if isinstance(output_value, str):
179
- try:
180
- parsed = json.loads(output_value)
181
- if isinstance(parsed, dict) and "response" in parsed:
182
- response = parsed.get("response", output_value)
183
- else:
184
- response = output_value
185
- except json.JSONDecodeError:
186
- response = output_value
187
- else:
188
- response = output_value
189
-
190
- return ToolCallInfo(
191
- tool_name=tool_name,
192
- tool_call_id=tool_call_id,
193
- arguments=arguments,
194
- response=response,
195
- )
196
-
197
-
198
- def extract_tool_openai_agents(span: dict[str, Any]) -> ToolCallInfo | None:
199
- """
200
- Extract tool info from OpenAI Agents tool spans.
201
-
202
- Attributes:
203
- - tool.name: "multiply"
204
- - input.value: "{\"a\":10,\"b\":2}"
205
- - output.value: 20.0
206
- """
207
- attrs = span.get("attributes", {})
208
-
209
- tool_name = attrs.get("tool.name")
210
- if not tool_name:
211
- return None
212
-
213
- arguments = attrs.get("input.value", "{}")
214
- response = attrs.get("output.value", "")
215
-
216
- return ToolCallInfo(
217
- tool_name=tool_name,
218
- tool_call_id=None, # OpenAI Agents SDK doesn't put call ID in tool span
219
- arguments=arguments,
220
- response=response,
221
- )
222
-
223
-
224
- def extract_tool_agno(span: dict[str, Any]) -> ToolCallInfo | None:
225
- """
226
- Extract tool info from Agno tool spans.
227
-
228
- Attributes:
229
- - tool.name: "duckduckgo_search"
230
- - tool.description: "..."
231
- - tool.parameters: "{\"query\": \"...\", \"max_results\": 5}"
232
- - input.value: same as parameters
233
- - output.value: JSON response
234
- """
235
- attrs = span.get("attributes", {})
236
-
237
- tool_name = attrs.get("tool.name")
238
- if not tool_name:
239
- return None
240
-
241
- arguments = attrs.get("tool.parameters") or attrs.get("input.value", "{}")
242
- response = attrs.get("output.value", "")
243
-
244
- return ToolCallInfo(
245
- tool_name=tool_name,
246
- tool_call_id=None,
247
- arguments=arguments,
248
- response=response,
249
- )
250
-
251
-
252
- def extract_tool_smolagents(span: dict[str, Any]) -> ToolCallInfo | None:
253
- """
254
- Extract tool info from SmolAgents tool spans.
255
- """
256
- attrs = span.get("attributes", {})
257
-
258
- tool_name = attrs.get("tool.name")
259
- if not tool_name:
260
- return None
261
-
262
- arguments = attrs.get("input.value", "{}")
263
- response = attrs.get("output.value", "")
264
-
265
- return ToolCallInfo(
266
- tool_name=tool_name,
267
- tool_call_id=None,
268
- arguments=arguments,
269
- response=response,
270
- )
271
-
272
-
273
- # Registry of framework extractors
274
- TOOL_EXTRACTORS: dict[Framework, ToolExtractor] = {
275
- Framework.PYDANTIC: extract_tool_pydantic,
276
- Framework.LANGGRAPH: extract_tool_langgraph,
277
- Framework.GOOGLE_ADK: extract_tool_adk,
278
- Framework.OPENAI_AGENTS: extract_tool_openai_agents,
279
- Framework.AGNO: extract_tool_agno,
280
- Framework.SMOLAGENTS: extract_tool_smolagents,
281
- }
282
-
283
-
284
- def get_tool_extractor(framework: Framework | str) -> ToolExtractor:
285
- """Get the appropriate tool extractor for the given framework."""
286
- if isinstance(framework, str):
287
- framework = Framework(framework.lower())
288
- return TOOL_EXTRACTORS.get(
289
- framework, extract_tool_langgraph
290
- ) # Default to langgraph
1
+ """
2
+ Framework-specific tool extractors for converting span attributes to standardized tool call information.
3
+
4
+ These extractors handle the varying attribute structures across different agent frameworks
5
+ (pydantic, langchain, adk, openai_agents, agno, smolagents, etc.)
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Protocol
10
+
11
+ from openinference.semconv.trace import SpanAttributes
12
+
13
+ from quraite.tracing.constants import Framework
14
+
15
+
16
+ class ToolCallInfo:
17
+ """Standardized tool call information extracted from a TOOL span."""
18
+
19
+ def __init__(
20
+ self,
21
+ tool_name: str,
22
+ tool_call_id: str | None,
23
+ arguments: str | dict,
24
+ response: Any,
25
+ ):
26
+ self.tool_name = tool_name
27
+ self.tool_call_id = tool_call_id
28
+ self.arguments = arguments
29
+ self.response = response
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "role": "tool",
34
+ "tool_name": self.tool_name,
35
+ "tool_call_id": self.tool_call_id,
36
+ "arguments": self.arguments,
37
+ "response": self.response,
38
+ }
39
+
40
+
41
+ class ToolExtractor(Protocol):
42
+ """Protocol for framework-specific tool extractors."""
43
+
44
+ def __call__(self, span: dict[str, Any]) -> ToolCallInfo | None: ...
45
+
46
+
47
+ # =============================================================================
48
+ # Framework-specific tool extractors
49
+ # =============================================================================
50
+
51
+
52
+ def extract_tool_pydantic(span: dict[str, Any]) -> ToolCallInfo | None:
53
+ """
54
+ Extract tool info from Pydantic AI tool spans.
55
+
56
+ Attributes:
57
+ - tool.name: "customer_balance"
58
+ - tool_call.id: "call_xxx"
59
+ - tool_arguments: "{\"include_pending\":true}"
60
+ - tool_response: "$123.45"
61
+ """
62
+ attrs = span.get("attributes", {})
63
+
64
+ tool_name = attrs.get("tool.name") or attrs.get("gen_ai.tool.name")
65
+ if not tool_name:
66
+ return None
67
+
68
+ tool_call_id = attrs.get("tool_call.id") or attrs.get("gen_ai.tool.call.id")
69
+ arguments = attrs.get("tool_arguments", "{}")
70
+ response = attrs.get("tool_response", "")
71
+
72
+ return ToolCallInfo(
73
+ tool_name=tool_name,
74
+ tool_call_id=tool_call_id,
75
+ arguments=arguments,
76
+ response=response,
77
+ )
78
+
79
+
80
+ def extract_tool_langchain(span: dict[str, Any]) -> ToolCallInfo | None:
81
+ """
82
+ Extract tool info from LangChain tool spans.
83
+
84
+ Attributes:
85
+ - tool.name: "add"
86
+ - tool.description: "Add two numbers."
87
+ - input.value: "{'b': 1, 'a': 1}"
88
+ - output.value: JSON with content
89
+ """
90
+ attrs = span.get("attributes", {})
91
+
92
+ tool_name = attrs.get("tool.name")
93
+ if not tool_name:
94
+ return None
95
+
96
+ arguments = attrs.get("input.value", "{}")
97
+ output_value = attrs.get("output.value", "")
98
+
99
+ # Also check for response attribute (some LangChain spans store response here)
100
+ response_value = attrs.get("response", output_value)
101
+
102
+ # Try to parse output to extract content
103
+ response = response_value
104
+ if isinstance(response_value, str):
105
+ try:
106
+ parsed = json.loads(response_value)
107
+ if isinstance(parsed, dict):
108
+ # Check if response field contains JSON string (nested JSON)
109
+ if "response" in parsed and isinstance(parsed["response"], str):
110
+ try:
111
+ inner_parsed = json.loads(parsed["response"])
112
+ if isinstance(inner_parsed, dict) and "update" in inner_parsed:
113
+ parsed = inner_parsed
114
+ except (json.JSONDecodeError, TypeError):
115
+ pass
116
+
117
+ # First check for direct content field
118
+ if "content" in parsed:
119
+ response = parsed.get("content", response_value)
120
+ # Check for update.messages structure (LangChain graph updates)
121
+ # this comes when you use supervisor agent with multiple agents
122
+ elif "update" in parsed:
123
+ update = parsed.get("update", {})
124
+ messages = update.get("messages", [])
125
+ # Find the last tool message
126
+ for msg in reversed(messages):
127
+ if isinstance(msg, dict) and msg.get("type") == "tool":
128
+ content = msg.get("content", "")
129
+ if content:
130
+ response = content
131
+ break
132
+ else:
133
+ # No tool message found, keep original response
134
+ response = response_value
135
+ else:
136
+ response = response_value
137
+ except json.JSONDecodeError:
138
+ pass
139
+
140
+ return ToolCallInfo(
141
+ tool_name=tool_name,
142
+ tool_call_id=None, # LangChain doesn't always have call IDs in tool spans
143
+ arguments=arguments,
144
+ response=response,
145
+ )
146
+
147
+
148
+ def extract_tool_adk(span: dict[str, Any]) -> ToolCallInfo | None:
149
+ """
150
+ Extract tool info from Google ADK tool spans.
151
+
152
+ Attributes:
153
+ - tool.name: "get_weather"
154
+ - tool.parameters: "{\"city\": \"New York\"}"
155
+ - gcp.vertex.agent.tool_call_args: "{\"city\": \"New York\"}"
156
+ - gcp.vertex.agent.tool_response: JSON response
157
+ - output.value: JSON with id, name, response
158
+ """
159
+ attrs = span.get("attributes", {})
160
+
161
+ tool_name = attrs.get("tool.name") or attrs.get("gen_ai.tool.name")
162
+ if not tool_name:
163
+ return None
164
+
165
+ # Skip merged tool spans
166
+ if tool_name == "(merged tools)":
167
+ return None
168
+
169
+ tool_call_id = attrs.get("gen_ai.tool.call.id")
170
+ arguments = (
171
+ attrs.get("tool.parameters")
172
+ or attrs.get("gcp.vertex.agent.tool_call_args")
173
+ or attrs.get("input.value", "{}")
174
+ )
175
+
176
+ # Get response from various possible locations
177
+ response = attrs.get("gcp.vertex.agent.tool_response", "")
178
+ if not response or response == "<not serializable>":
179
+ output_value = attrs.get("output.value", "")
180
+ if isinstance(output_value, str):
181
+ try:
182
+ parsed = json.loads(output_value)
183
+ if isinstance(parsed, dict) and "response" in parsed:
184
+ response = parsed.get("response", output_value)
185
+ else:
186
+ response = output_value
187
+ except json.JSONDecodeError:
188
+ response = output_value
189
+ else:
190
+ response = output_value
191
+
192
+ return ToolCallInfo(
193
+ tool_name=tool_name,
194
+ tool_call_id=tool_call_id,
195
+ arguments=arguments,
196
+ response=response,
197
+ )
198
+
199
+
200
+ def extract_tool_openai_agents(span: dict[str, Any]) -> ToolCallInfo | None:
201
+ """
202
+ Extract tool info from OpenAI Agents tool spans.
203
+
204
+ Attributes:
205
+ - tool.name: "multiply"
206
+ - input.value: "{\"a\":10,\"b\":2}"
207
+ - output.value: 20.0
208
+ """
209
+ attrs = span.get("attributes", {})
210
+
211
+ tool_name = attrs.get("tool.name")
212
+ if not tool_name:
213
+ return None
214
+
215
+ arguments = attrs.get("input.value", "{}")
216
+ response = attrs.get("output.value", "")
217
+
218
+ return ToolCallInfo(
219
+ tool_name=tool_name,
220
+ tool_call_id=None, # OpenAI Agents SDK doesn't put call ID in tool span
221
+ arguments=arguments,
222
+ response=response,
223
+ )
224
+
225
+
226
+ def extract_tool_agno(span: dict[str, Any]) -> ToolCallInfo | None:
227
+ """
228
+ Extract tool info from Agno tool spans.
229
+
230
+ Attributes:
231
+ - tool.name: "duckduckgo_search"
232
+ - tool.description: "..."
233
+ - tool.parameters: "{\"query\": \"...\", \"max_results\": 5}"
234
+ - input.value: same as parameters
235
+ - output.value: JSON response
236
+ """
237
+ attrs = span.get("attributes", {})
238
+
239
+ tool_name = attrs.get("tool.name")
240
+ if not tool_name:
241
+ return None
242
+
243
+ arguments = attrs.get("tool.parameters") or attrs.get("input.value", "{}")
244
+ response = attrs.get("output.value", "")
245
+
246
+ return ToolCallInfo(
247
+ tool_name=tool_name,
248
+ tool_call_id=None,
249
+ arguments=arguments,
250
+ response=response,
251
+ )
252
+
253
+
254
+ def extract_tool_smolagents(span: dict[str, Any]) -> ToolCallInfo | None:
255
+ """
256
+ Extract tool info from SmolAgents tool spans.
257
+ """
258
+ attrs = span.get("attributes", {})
259
+
260
+ tool_name = attrs.get("tool.name")
261
+ if not tool_name:
262
+ return None
263
+
264
+ arguments = attrs.get("input.value", "{}")
265
+ response = attrs.get("output.value", "")
266
+
267
+ return ToolCallInfo(
268
+ tool_name=tool_name,
269
+ tool_call_id=None,
270
+ arguments=arguments,
271
+ response=response,
272
+ )
273
+
274
+
275
+ def extract_default(span: dict[str, Any]) -> ToolCallInfo | None:
276
+ """
277
+ Extract tool info following openinference semantic conventions.
278
+ """
279
+ attrs = span.get("attributes", {})
280
+
281
+ tool_name = attrs.get(SpanAttributes.TOOL_NAME, "")
282
+ arguments = attrs.get(SpanAttributes.TOOL_PARAMETERS, "{}")
283
+ response = attrs.get(SpanAttributes.OUTPUT_VALUE, "")
284
+
285
+ return ToolCallInfo(
286
+ tool_name=tool_name,
287
+ tool_call_id=None,
288
+ arguments=arguments,
289
+ response=response,
290
+ )
291
+
292
+
293
+ # Registry of framework extractors
294
+ TOOL_EXTRACTORS: dict[Framework, ToolExtractor] = {
295
+ Framework.PYDANTIC_AI: extract_tool_pydantic,
296
+ Framework.LANGCHAIN: extract_tool_langchain,
297
+ Framework.GOOGLE_ADK: extract_tool_adk,
298
+ Framework.OPENAI_AGENTS: extract_tool_openai_agents,
299
+ Framework.AGNO: extract_tool_agno,
300
+ Framework.SMOLAGENTS: extract_tool_smolagents,
301
+ Framework.DEFAULT: extract_default,
302
+ }
303
+
304
+
305
+ def get_tool_extractor(framework: Framework | str) -> ToolExtractor:
306
+ """Get the appropriate tool extractor for the given framework."""
307
+ if isinstance(framework, str):
308
+ framework = Framework(framework.lower())
309
+ return TOOL_EXTRACTORS.get(framework, extract_default)