quraite 0.0.2__py3-none-any.whl → 0.1.0__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 (48) hide show
  1. quraite/__init__.py +3 -3
  2. quraite/adapters/__init__.py +134 -134
  3. quraite/adapters/agno_adapter.py +159 -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 +209 -209
  8. quraite/adapters/http_adapter.py +239 -239
  9. quraite/adapters/langflow_adapter.py +192 -192
  10. quraite/adapters/langgraph_adapter.py +304 -304
  11. quraite/adapters/langgraph_server_adapter.py +252 -252
  12. quraite/adapters/n8n_adapter.py +220 -220
  13. quraite/adapters/openai_agents_adapter.py +269 -269
  14. quraite/adapters/pydantic_ai_adapter.py +312 -312
  15. quraite/adapters/smolagents_adapter.py +152 -152
  16. quraite/logger.py +61 -64
  17. quraite/schema/message.py +91 -54
  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/tracing/__init__.py +24 -24
  23. quraite/tracing/constants.py +16 -16
  24. quraite/tracing/span_exporter.py +115 -115
  25. quraite/tracing/span_processor.py +49 -49
  26. quraite/tracing/tool_extractors.py +290 -290
  27. quraite/tracing/trace.py +564 -494
  28. quraite/tracing/types.py +179 -179
  29. quraite/tracing/utils.py +170 -170
  30. quraite/utils/json_utils.py +269 -269
  31. {quraite-0.0.2.dist-info → quraite-0.1.0.dist-info}/METADATA +9 -9
  32. quraite-0.1.0.dist-info/RECORD +35 -0
  33. {quraite-0.0.2.dist-info → quraite-0.1.0.dist-info}/WHEEL +1 -1
  34. quraite/traces/traces_adk_openinference.json +0 -379
  35. quraite/traces/traces_agno_multi_agent.json +0 -669
  36. quraite/traces/traces_agno_openinference.json +0 -321
  37. quraite/traces/traces_crewai_openinference.json +0 -155
  38. quraite/traces/traces_langgraph_openinference.json +0 -349
  39. quraite/traces/traces_langgraph_openinference_multi_agent.json +0 -2705
  40. quraite/traces/traces_langgraph_traceloop.json +0 -510
  41. quraite/traces/traces_openai_agents_multi_agent_1.json +0 -402
  42. quraite/traces/traces_openai_agents_openinference.json +0 -341
  43. quraite/traces/traces_pydantic_openinference.json +0 -286
  44. quraite/traces/traces_pydantic_openinference_multi_agent_1.json +0 -399
  45. quraite/traces/traces_pydantic_openinference_multi_agent_2.json +0 -398
  46. quraite/traces/traces_smol_agents_openinference.json +0 -397
  47. quraite/traces/traces_smol_agents_tool_calling_openinference.json +0 -704
  48. quraite-0.0.2.dist-info/RECORD +0 -49
@@ -1,275 +1,275 @@
1
- import asyncio
2
- import json
3
- from typing import Any, Dict, List, Optional, Union
4
-
5
- import aiohttp
6
-
7
- from quraite.adapters.base import BaseAdapter
8
- from quraite.logger import get_logger
9
- from quraite.schema.message import (
10
- AgentMessage,
11
- AssistantMessage,
12
- MessageContentText,
13
- ToolCall,
14
- ToolMessage,
15
- )
16
- from quraite.schema.response import AgentInvocationResponse
17
-
18
- logger = get_logger(__name__)
19
-
20
-
21
- class FlowiseAdapter(BaseAdapter):
22
- def __init__(
23
- self, api_url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 60
24
- ):
25
- self.api_url = api_url
26
- self.headers = headers or {}
27
- self.timeout = timeout
28
-
29
- if "Content-Type" not in self.headers:
30
- self.headers["Content-Type"] = "application/json"
31
-
32
- self.headers["Content-Type"] = "application/json"
33
- logger.info(
34
- "FlowiseAdapter initialized (api_url=%s, timeout=%s)", self.api_url, timeout
35
- )
36
-
37
- AGENT_NODE_NAME = "agentAgentflow"
38
-
39
- def _convert_api_output_to_messages(
40
- self,
41
- response: Dict[str, Any],
42
- request_messages: Optional[List[AgentMessage]] = None,
43
- ) -> List[AgentMessage]:
44
- logger.debug(
45
- "Converting Flowise response to messages (has_agent_node=%s)",
46
- bool(response.get("agentFlowExecutedData")),
47
- )
48
-
49
- def _append_text(content_obj: Any, bucket: List[MessageContentText]) -> None:
50
- if isinstance(content_obj, str):
51
- if content_obj:
52
- bucket.append(MessageContentText(type="text", text=content_obj))
53
- elif isinstance(content_obj, list):
54
- for item in content_obj:
55
- _append_text(item, bucket)
56
- elif isinstance(content_obj, dict):
57
- text_value = content_obj.get("text")
58
- if text_value:
59
- bucket.append(MessageContentText(type="text", text=text_value))
60
-
61
- agent_node = self._find_agentflow_node(response)
62
- if agent_node:
63
- converted: List[AgentMessage] = []
64
- messages_section = agent_node.get("input")
65
- raw_messages = []
66
- if isinstance(messages_section, dict):
67
- potential_messages = messages_section.get("messages")
68
- if isinstance(potential_messages, list):
69
- raw_messages = potential_messages
70
-
71
- latest_turn: List[Dict[str, Any]] = []
72
- for raw in reversed(raw_messages):
73
- if not isinstance(raw, dict):
74
- continue
75
- if raw.get("role") == "user":
76
- break
77
- latest_turn.append(raw)
78
- latest_turn.reverse()
79
-
80
- for raw in latest_turn:
81
- role = raw.get("role")
82
- if role not in {"assistant", "system", "tool"}:
83
- continue
84
-
85
- if role == "assistant":
86
- text_content: List[MessageContentText] = []
87
- _append_text(raw.get("content"), text_content)
88
- tool_calls_list: List[ToolCall] = []
89
- tool_calls = raw.get("tool_calls")
90
- if isinstance(tool_calls, list):
91
- for call in tool_calls:
92
- if not isinstance(call, dict):
93
- continue
94
- arguments = call.get("args")
95
- if not isinstance(arguments, dict):
96
- arguments = {"value": arguments}
97
- tool_calls_list.append(
98
- ToolCall(
99
- id=call.get("id"),
100
- name=call.get("name"),
101
- arguments=arguments,
102
- )
103
- )
104
-
105
- converted.append(
106
- AssistantMessage(
107
- content=text_content if text_content else None,
108
- tool_calls=tool_calls_list if tool_calls_list else None,
109
- )
110
- )
111
-
112
- elif role == "tool":
113
- payload = raw.get("content")
114
- tool_result = (
115
- payload if isinstance(payload, dict) else {"output": payload}
116
- )
117
- converted.append(
118
- ToolMessage(
119
- tool_name=raw.get("name"),
120
- tool_call_id=raw.get("tool_call_id"),
121
- content=[
122
- MessageContentText(
123
- type="text", text=json.dumps(tool_result)
124
- )
125
- ],
126
- )
127
- )
128
-
129
- output_section = agent_node.get("output")
130
- if isinstance(output_section, dict):
131
- final_segments: List[MessageContentText] = []
132
- _append_text(output_section.get("content"), final_segments)
133
-
134
- if final_segments:
135
- converted.append(AssistantMessage(content=final_segments))
136
-
137
- if converted:
138
- return converted
139
-
140
- if request_messages:
141
- logger.debug("Falling back to request messages; no agent output found")
142
- return list(request_messages)
143
-
144
- return []
145
-
146
- def _find_agentflow_node(
147
- self, response: Dict[str, Any]
148
- ) -> Optional[Dict[str, Any]]:
149
- executed_data = response.get("agentFlowExecutedData")
150
- if not isinstance(executed_data, list):
151
- return None
152
-
153
- for entry in executed_data:
154
- node_data = entry.get("data")
155
- if not isinstance(node_data, dict):
156
- continue
157
-
158
- if node_data.get("name") == self.AGENT_NODE_NAME:
159
- return node_data
160
-
161
- return None
162
-
163
- def _prepare_input(self, input: List[AgentMessage]) -> str:
164
- logger.debug("Preparing Flowise input from %d messages", len(input))
165
- if not input or input[-1].role != "user":
166
- logger.error("Flowise input missing user message")
167
- raise ValueError("No user message found in the input")
168
-
169
- last_user_message = input[-1]
170
- if not last_user_message.content:
171
- logger.error("Flowise input user message missing content")
172
- raise ValueError("User message has no content")
173
-
174
- text_content = None
175
- for content_item in last_user_message.content:
176
- if content_item.type == "text" and content_item.text:
177
- text_content = content_item.text
178
- break
179
-
180
- if not text_content:
181
- logger.error("Flowise input missing text content")
182
- raise ValueError("No text content found in user message")
183
-
184
- logger.debug("Prepared Flowise input (text_length=%d)", len(text_content))
185
- return text_content
186
-
187
- async def _aapi_call(
188
- self,
189
- question: str,
190
- history: Optional[List[Dict[str, Any]]] = None,
191
- override_config: Optional[Dict[str, Any]] = None,
192
- form: Optional[Dict[str, Any]] = None,
193
- human_input: Optional[str] = None,
194
- **kwargs,
195
- ) -> Dict[str, Any]:
196
- logger.debug(
197
- "Calling Flowise API (history=%s, form=%s, human_input=%s)",
198
- bool(history),
199
- bool(form),
200
- bool(human_input),
201
- )
202
- payload = {
203
- "question": question,
204
- "overrideConfig": override_config or {},
205
- **kwargs,
206
- }
207
-
208
- if history is not None:
209
- payload["history"] = history
210
-
211
- if form is not None:
212
- payload["form"] = form
213
-
214
- if human_input is not None:
215
- payload["humanInput"] = human_input
216
-
217
- async with aiohttp.ClientSession() as session:
218
- try:
219
- async with session.post(
220
- self.api_url,
221
- headers=self.headers,
222
- json=payload,
223
- timeout=aiohttp.ClientTimeout(total=self.timeout),
224
- ) as response:
225
- response.raise_for_status()
226
- logger.info(
227
- "Flowise API call succeeded (status=%s)", response.status
228
- )
229
- return await response.json()
230
-
231
- except (aiohttp.ClientError, asyncio.TimeoutError) as e:
232
- logger.exception("Flowise API request failed")
233
- raise aiohttp.ClientError(f"Async API request failed: {str(e)}") from e
234
-
235
- except json.JSONDecodeError as e:
236
- logger.exception("Flowise API response decoding failed")
237
- raise ValueError(f"Failed to decode JSON response: {e}") from e
238
-
239
- async def ainvoke(
240
- self,
241
- input: List[AgentMessage],
242
- session_id: Union[str, None],
243
- ) -> AgentInvocationResponse:
244
- """Asynchronous invocation method - invokes the Flowise agent and converts to List[AgentMessage]."""
245
- logger.info(
246
- "Flowise ainvoke called (session_id=%s, input_messages=%d)",
247
- session_id,
248
- len(input),
249
- )
250
- agent_input = self._prepare_input(input)
251
-
252
- try:
253
- agent_output = await self._aapi_call(
254
- question=agent_input,
255
- override_config={"sessionId": session_id} if session_id else None,
256
- )
257
- logger.debug(
258
- "Flowise API returned payload with keys: %s", list(agent_output.keys())
259
- )
260
- except Exception as e:
261
- logger.exception("Flowise ainvoke failed during API call")
262
- raise RuntimeError(f"Error calling Flowise endpoint: {e}") from e
263
-
264
- try:
265
- agent_trajectory = self._convert_api_output_to_messages(agent_output, input)
266
- logger.info(
267
- "Flowise conversion produced %d trajectory messages",
268
- len(agent_trajectory),
269
- )
270
- return AgentInvocationResponse(
271
- agent_trajectory=agent_trajectory,
272
- )
273
- except Exception as e:
274
- logger.exception("Flowise ainvoke failed while processing response")
275
- raise RuntimeError(f"Error processing Flowise response: {e}") from e
1
+ import asyncio
2
+ import json
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ import aiohttp
6
+
7
+ from quraite.adapters.base import BaseAdapter
8
+ from quraite.logger import get_logger
9
+ from quraite.schema.message import (
10
+ AgentMessage,
11
+ AssistantMessage,
12
+ MessageContentText,
13
+ ToolCall,
14
+ ToolMessage,
15
+ )
16
+ from quraite.schema.response import AgentInvocationResponse
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class FlowiseAdapter(BaseAdapter):
22
+ def __init__(
23
+ self, api_url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 60
24
+ ):
25
+ self.api_url = api_url
26
+ self.headers = headers or {}
27
+ self.timeout = timeout
28
+
29
+ if "Content-Type" not in self.headers:
30
+ self.headers["Content-Type"] = "application/json"
31
+
32
+ self.headers["Content-Type"] = "application/json"
33
+ logger.info(
34
+ "FlowiseAdapter initialized (api_url=%s, timeout=%s)", self.api_url, timeout
35
+ )
36
+
37
+ AGENT_NODE_NAME = "agentAgentflow"
38
+
39
+ def _convert_api_output_to_messages(
40
+ self,
41
+ response: Dict[str, Any],
42
+ request_messages: Optional[List[AgentMessage]] = None,
43
+ ) -> List[AgentMessage]:
44
+ logger.debug(
45
+ "Converting Flowise response to messages (has_agent_node=%s)",
46
+ bool(response.get("agentFlowExecutedData")),
47
+ )
48
+
49
+ def _append_text(content_obj: Any, bucket: List[MessageContentText]) -> None:
50
+ if isinstance(content_obj, str):
51
+ if content_obj:
52
+ bucket.append(MessageContentText(type="text", text=content_obj))
53
+ elif isinstance(content_obj, list):
54
+ for item in content_obj:
55
+ _append_text(item, bucket)
56
+ elif isinstance(content_obj, dict):
57
+ text_value = content_obj.get("text")
58
+ if text_value:
59
+ bucket.append(MessageContentText(type="text", text=text_value))
60
+
61
+ agent_node = self._find_agentflow_node(response)
62
+ if agent_node:
63
+ converted: List[AgentMessage] = []
64
+ messages_section = agent_node.get("input")
65
+ raw_messages = []
66
+ if isinstance(messages_section, dict):
67
+ potential_messages = messages_section.get("messages")
68
+ if isinstance(potential_messages, list):
69
+ raw_messages = potential_messages
70
+
71
+ latest_turn: List[Dict[str, Any]] = []
72
+ for raw in reversed(raw_messages):
73
+ if not isinstance(raw, dict):
74
+ continue
75
+ if raw.get("role") == "user":
76
+ break
77
+ latest_turn.append(raw)
78
+ latest_turn.reverse()
79
+
80
+ for raw in latest_turn:
81
+ role = raw.get("role")
82
+ if role not in {"assistant", "system", "tool"}:
83
+ continue
84
+
85
+ if role == "assistant":
86
+ text_content: List[MessageContentText] = []
87
+ _append_text(raw.get("content"), text_content)
88
+ tool_calls_list: List[ToolCall] = []
89
+ tool_calls = raw.get("tool_calls")
90
+ if isinstance(tool_calls, list):
91
+ for call in tool_calls:
92
+ if not isinstance(call, dict):
93
+ continue
94
+ arguments = call.get("args")
95
+ if not isinstance(arguments, dict):
96
+ arguments = {"value": arguments}
97
+ tool_calls_list.append(
98
+ ToolCall(
99
+ id=call.get("id"),
100
+ name=call.get("name"),
101
+ arguments=arguments,
102
+ )
103
+ )
104
+
105
+ converted.append(
106
+ AssistantMessage(
107
+ content=text_content if text_content else None,
108
+ tool_calls=tool_calls_list if tool_calls_list else None,
109
+ )
110
+ )
111
+
112
+ elif role == "tool":
113
+ payload = raw.get("content")
114
+ tool_result = (
115
+ payload if isinstance(payload, dict) else {"output": payload}
116
+ )
117
+ converted.append(
118
+ ToolMessage(
119
+ tool_name=raw.get("name"),
120
+ tool_call_id=raw.get("tool_call_id"),
121
+ content=[
122
+ MessageContentText(
123
+ type="text", text=json.dumps(tool_result)
124
+ )
125
+ ],
126
+ )
127
+ )
128
+
129
+ output_section = agent_node.get("output")
130
+ if isinstance(output_section, dict):
131
+ final_segments: List[MessageContentText] = []
132
+ _append_text(output_section.get("content"), final_segments)
133
+
134
+ if final_segments:
135
+ converted.append(AssistantMessage(content=final_segments))
136
+
137
+ if converted:
138
+ return converted
139
+
140
+ if request_messages:
141
+ logger.debug("Falling back to request messages; no agent output found")
142
+ return list(request_messages)
143
+
144
+ return []
145
+
146
+ def _find_agentflow_node(
147
+ self, response: Dict[str, Any]
148
+ ) -> Optional[Dict[str, Any]]:
149
+ executed_data = response.get("agentFlowExecutedData")
150
+ if not isinstance(executed_data, list):
151
+ return None
152
+
153
+ for entry in executed_data:
154
+ node_data = entry.get("data")
155
+ if not isinstance(node_data, dict):
156
+ continue
157
+
158
+ if node_data.get("name") == self.AGENT_NODE_NAME:
159
+ return node_data
160
+
161
+ return None
162
+
163
+ def _prepare_input(self, input: List[AgentMessage]) -> str:
164
+ logger.debug("Preparing Flowise input from %d messages", len(input))
165
+ if not input or input[-1].role != "user":
166
+ logger.error("Flowise input missing user message")
167
+ raise ValueError("No user message found in the input")
168
+
169
+ last_user_message = input[-1]
170
+ if not last_user_message.content:
171
+ logger.error("Flowise input user message missing content")
172
+ raise ValueError("User message has no content")
173
+
174
+ text_content = None
175
+ for content_item in last_user_message.content:
176
+ if content_item.type == "text" and content_item.text:
177
+ text_content = content_item.text
178
+ break
179
+
180
+ if not text_content:
181
+ logger.error("Flowise input missing text content")
182
+ raise ValueError("No text content found in user message")
183
+
184
+ logger.debug("Prepared Flowise input (text_length=%d)", len(text_content))
185
+ return text_content
186
+
187
+ async def _aapi_call(
188
+ self,
189
+ question: str,
190
+ history: Optional[List[Dict[str, Any]]] = None,
191
+ override_config: Optional[Dict[str, Any]] = None,
192
+ form: Optional[Dict[str, Any]] = None,
193
+ human_input: Optional[str] = None,
194
+ **kwargs,
195
+ ) -> Dict[str, Any]:
196
+ logger.debug(
197
+ "Calling Flowise API (history=%s, form=%s, human_input=%s)",
198
+ bool(history),
199
+ bool(form),
200
+ bool(human_input),
201
+ )
202
+ payload = {
203
+ "question": question,
204
+ "overrideConfig": override_config or {},
205
+ **kwargs,
206
+ }
207
+
208
+ if history is not None:
209
+ payload["history"] = history
210
+
211
+ if form is not None:
212
+ payload["form"] = form
213
+
214
+ if human_input is not None:
215
+ payload["humanInput"] = human_input
216
+
217
+ async with aiohttp.ClientSession() as session:
218
+ try:
219
+ async with session.post(
220
+ self.api_url,
221
+ headers=self.headers,
222
+ json=payload,
223
+ timeout=aiohttp.ClientTimeout(total=self.timeout),
224
+ ) as response:
225
+ response.raise_for_status()
226
+ logger.info(
227
+ "Flowise API call succeeded (status=%s)", response.status
228
+ )
229
+ return await response.json()
230
+
231
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
232
+ logger.exception("Flowise API request failed")
233
+ raise aiohttp.ClientError(f"Async API request failed: {str(e)}") from e
234
+
235
+ except json.JSONDecodeError as e:
236
+ logger.exception("Flowise API response decoding failed")
237
+ raise ValueError(f"Failed to decode JSON response: {e}") from e
238
+
239
+ async def ainvoke(
240
+ self,
241
+ input: List[AgentMessage],
242
+ session_id: Union[str, None],
243
+ ) -> AgentInvocationResponse:
244
+ """Asynchronous invocation method - invokes the Flowise agent and converts to List[AgentMessage]."""
245
+ logger.info(
246
+ "Flowise ainvoke called (session_id=%s, input_messages=%d)",
247
+ session_id,
248
+ len(input),
249
+ )
250
+ agent_input = self._prepare_input(input)
251
+
252
+ try:
253
+ agent_output = await self._aapi_call(
254
+ question=agent_input,
255
+ override_config={"sessionId": session_id} if session_id else None,
256
+ )
257
+ logger.debug(
258
+ "Flowise API returned payload with keys: %s", list(agent_output.keys())
259
+ )
260
+ except Exception as e:
261
+ logger.exception("Flowise ainvoke failed during API call")
262
+ raise RuntimeError(f"Error calling Flowise endpoint: {e}") from e
263
+
264
+ try:
265
+ agent_trajectory = self._convert_api_output_to_messages(agent_output, input)
266
+ logger.info(
267
+ "Flowise conversion produced %d trajectory messages",
268
+ len(agent_trajectory),
269
+ )
270
+ return AgentInvocationResponse(
271
+ agent_trajectory=agent_trajectory,
272
+ )
273
+ except Exception as e:
274
+ logger.exception("Flowise ainvoke failed while processing response")
275
+ raise RuntimeError(f"Error processing Flowise response: {e}") from e