quraite 0.0.2__py3-none-any.whl → 0.1.1__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.
- quraite/adapters/http_adapter.py +46 -34
- quraite/logger.py +5 -8
- quraite/schema/message.py +38 -1
- quraite/serve/local_agent.py +3 -3
- quraite/tracing/trace.py +80 -10
- quraite-0.1.1.dist-info/METADATA +377 -0
- {quraite-0.0.2.dist-info → quraite-0.1.1.dist-info}/RECORD +8 -8
- quraite-0.0.2.dist-info/METADATA +0 -44
- {quraite-0.0.2.dist-info → quraite-0.1.1.dist-info}/WHEEL +0 -0
quraite/adapters/http_adapter.py
CHANGED
|
@@ -118,10 +118,6 @@ class HttpAdapter(BaseAdapter):
|
|
|
118
118
|
|
|
119
119
|
Returns:
|
|
120
120
|
Response data as dictionary
|
|
121
|
-
|
|
122
|
-
Raises:
|
|
123
|
-
httpx.HTTPError: If all retries fail
|
|
124
|
-
ValueError: If response format is invalid
|
|
125
121
|
"""
|
|
126
122
|
last_exception = None
|
|
127
123
|
|
|
@@ -141,28 +137,15 @@ class HttpAdapter(BaseAdapter):
|
|
|
141
137
|
return response.json()
|
|
142
138
|
|
|
143
139
|
except httpx.HTTPStatusError as e:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
raise ValueError(
|
|
153
|
-
f"Agent server error ({e.response.status_code}): {error_detail}"
|
|
154
|
-
) from e
|
|
140
|
+
error_detail = e.response.text
|
|
141
|
+
logger.error(
|
|
142
|
+
"Failed while invoking the url '%s' with status code '%s' and detail '%s'",
|
|
143
|
+
url,
|
|
144
|
+
e.response.status_code,
|
|
145
|
+
error_detail,
|
|
146
|
+
)
|
|
155
147
|
|
|
156
|
-
|
|
157
|
-
last_exception = e
|
|
158
|
-
if attempt < self.max_retries - 1:
|
|
159
|
-
delay = self.retry_delay * (2**attempt)
|
|
160
|
-
logger.warning(
|
|
161
|
-
"HTTP adapter retrying after server error (status=%s, retry_in=%.2fs)",
|
|
162
|
-
e.response.status_code,
|
|
163
|
-
delay,
|
|
164
|
-
)
|
|
165
|
-
await self._async_sleep(delay)
|
|
148
|
+
raise e
|
|
166
149
|
|
|
167
150
|
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
168
151
|
# Retry on network errors
|
|
@@ -170,21 +153,27 @@ class HttpAdapter(BaseAdapter):
|
|
|
170
153
|
if attempt < self.max_retries - 1:
|
|
171
154
|
delay = self.retry_delay * (2**attempt)
|
|
172
155
|
logger.warning(
|
|
173
|
-
"HTTP adapter retrying after network error (retry_in=%.2fs)",
|
|
156
|
+
"HTTP adapter retrying after network/connection error (retry_in=%.2fs)",
|
|
174
157
|
delay,
|
|
175
158
|
)
|
|
176
159
|
await self._async_sleep(delay)
|
|
160
|
+
else:
|
|
161
|
+
logger.error(
|
|
162
|
+
"Failed while connecting to the url %s after %d attempts. Last error: %s",
|
|
163
|
+
url,
|
|
164
|
+
self.max_retries,
|
|
165
|
+
last_exception,
|
|
166
|
+
)
|
|
167
|
+
raise last_exception
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
logger.error(
|
|
170
|
+
"HTTP adapter failed while invoking the url %s after %d attempts. Last error: %s",
|
|
171
|
+
url,
|
|
181
172
|
self.max_retries,
|
|
182
|
-
|
|
173
|
+
last_exception,
|
|
183
174
|
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
f"{self.max_retries} attempts. Last error: {last_exception}"
|
|
187
|
-
) from last_exception
|
|
175
|
+
|
|
176
|
+
raise last_exception
|
|
188
177
|
|
|
189
178
|
@staticmethod
|
|
190
179
|
async def _async_sleep(seconds: float):
|
|
@@ -237,3 +226,26 @@ class HttpAdapter(BaseAdapter):
|
|
|
237
226
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
238
227
|
"""Async context manager exit."""
|
|
239
228
|
await self.aclose()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
import asyncio
|
|
233
|
+
import json
|
|
234
|
+
|
|
235
|
+
from quraite.schema.message import MessageContentText, UserMessage
|
|
236
|
+
|
|
237
|
+
async def test_http_adapter():
|
|
238
|
+
adapter = HttpAdapter(url="http://localhost:8080/v1/agents/completions")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
response = await adapter.ainvoke(
|
|
242
|
+
input=[UserMessage(content=[MessageContentText(text="What is 1 + 1")])],
|
|
243
|
+
session_id="test",
|
|
244
|
+
)
|
|
245
|
+
print(response)
|
|
246
|
+
except httpx.HTTPStatusError as e:
|
|
247
|
+
print(json.loads(e.response.text)["detail"])
|
|
248
|
+
except Exception as e:
|
|
249
|
+
print(e)
|
|
250
|
+
|
|
251
|
+
asyncio.run(test_http_adapter())
|
quraite/logger.py
CHANGED
|
@@ -54,11 +54,8 @@ def get_logger(name: str | None = None) -> logging.Logger:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# Set level from environment variable if provided
|
|
57
|
-
_env_level = os.getenv("LOG_LEVEL")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
pass
|
|
63
|
-
else:
|
|
64
|
-
set_log_level("INFO")
|
|
57
|
+
_env_level = os.getenv("LOG_LEVEL") or "INFO"
|
|
58
|
+
try:
|
|
59
|
+
set_log_level(_env_level)
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
quraite/schema/message.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any, List, Literal, Optional, TypeAlias, Union
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MessageContentText(BaseModel):
|
|
@@ -35,11 +35,47 @@ class ToolCall(BaseModel):
|
|
|
35
35
|
arguments: dict[str, Any]
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class ModelInfo(BaseModel):
|
|
39
|
+
model_name: str = Field(default="")
|
|
40
|
+
model_provider: str = Field(default="")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CostInfo(BaseModel):
|
|
44
|
+
input_cost: float = Field(default=0.0)
|
|
45
|
+
output_cost: float = Field(default=0.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TokenInfo(BaseModel):
|
|
49
|
+
input_tokens: int = Field(default=0)
|
|
50
|
+
output_tokens: int = Field(default=0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LatencyInfo(BaseModel):
|
|
54
|
+
start_time: float = Field(default=0.0)
|
|
55
|
+
end_time: float = Field(default=0.0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AssistantMessageMetadata(BaseModel):
|
|
59
|
+
"""Structured metadata for assistant messages."""
|
|
60
|
+
|
|
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)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolMessageMetadata(BaseModel):
|
|
68
|
+
"""Structured metadata for tool messages."""
|
|
69
|
+
|
|
70
|
+
latency: LatencyInfo = Field(default_factory=LatencyInfo)
|
|
71
|
+
|
|
72
|
+
|
|
38
73
|
class AssistantMessage(BaseModel):
|
|
39
74
|
role: Literal["assistant"] = "assistant"
|
|
40
75
|
agent_name: Optional[str] = None
|
|
41
76
|
content: Optional[List[Union[MessageContentText, MessageContentReasoning]]] = None
|
|
42
77
|
tool_calls: Optional[List[ToolCall]] = None
|
|
78
|
+
metadata: AssistantMessageMetadata = Field(default_factory=AssistantMessageMetadata)
|
|
43
79
|
|
|
44
80
|
|
|
45
81
|
class ToolMessage(BaseModel):
|
|
@@ -47,6 +83,7 @@ class ToolMessage(BaseModel):
|
|
|
47
83
|
tool_name: Optional[str] = None
|
|
48
84
|
tool_call_id: Optional[str] = None
|
|
49
85
|
content: List[MessageContentText]
|
|
86
|
+
metadata: ToolMessageMetadata = Field(default_factory=ToolMessageMetadata)
|
|
50
87
|
|
|
51
88
|
|
|
52
89
|
AgentMessage: TypeAlias = Union[
|
quraite/serve/local_agent.py
CHANGED
|
@@ -48,7 +48,7 @@ class LocalAgentServer:
|
|
|
48
48
|
Usage:
|
|
49
49
|
```python
|
|
50
50
|
from quraite.serve.local_agent_server import LocalAgentServer
|
|
51
|
-
from quraite.adapters
|
|
51
|
+
from quraite.adapters import LangGraphAdapter
|
|
52
52
|
|
|
53
53
|
sdk = LocalAgentServer(wrapped_agent=LangGraphAdapter(agent_graph=agent_graph))
|
|
54
54
|
sdk.start(host="0.0.0.0", port=8000, reload=False)
|
|
@@ -331,12 +331,12 @@ class LocalAgentServer:
|
|
|
331
331
|
|
|
332
332
|
try:
|
|
333
333
|
# Invoke agent (asynchronously)
|
|
334
|
-
|
|
334
|
+
response: AgentInvocationResponse = await self._agent.ainvoke(
|
|
335
335
|
input=request.input,
|
|
336
336
|
session_id=request.session_id,
|
|
337
337
|
)
|
|
338
338
|
|
|
339
|
-
return InvokeResponse(agent_response=
|
|
339
|
+
return InvokeResponse(agent_response=response)
|
|
340
340
|
|
|
341
341
|
except HTTPException:
|
|
342
342
|
raise
|
quraite/tracing/trace.py
CHANGED
|
@@ -11,13 +11,14 @@ from opentelemetry.sdk.trace import Span as OTelSpan
|
|
|
11
11
|
from pydantic import BaseModel, ConfigDict, Field
|
|
12
12
|
|
|
13
13
|
from quraite.logger import get_logger
|
|
14
|
-
from quraite.schema.message import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
from quraite.schema.message import AssistantMessage, AssistantMessageMetadata
|
|
15
|
+
from quraite.schema.message import CostInfo as MessageCostInfo
|
|
16
|
+
from quraite.schema.message import LatencyInfo as MessageLatencyInfo
|
|
17
|
+
from quraite.schema.message import MessageContentText
|
|
18
|
+
from quraite.schema.message import ModelInfo as MessageModelInfo
|
|
19
|
+
from quraite.schema.message import SystemMessage
|
|
20
|
+
from quraite.schema.message import TokenInfo as MessageTokenInfo
|
|
21
|
+
from quraite.schema.message import ToolCall, ToolMessage, ToolMessageMetadata
|
|
21
22
|
from quraite.tracing.constants import Framework
|
|
22
23
|
from quraite.tracing.types import Event, Link, Resource, SpanContext, SpanKind, Status
|
|
23
24
|
from quraite.tracing.utils import unflatten_messages
|
|
@@ -254,8 +255,15 @@ class AgentTrace(BaseModel):
|
|
|
254
255
|
self.spans.extend(spans)
|
|
255
256
|
self._invalidate_tokens_and_cost_cache()
|
|
256
257
|
|
|
257
|
-
def _convert_llm_message(
|
|
258
|
-
|
|
258
|
+
def _convert_llm_message(
|
|
259
|
+
self, msg: dict[str, Any], metadata: AssistantMessageMetadata
|
|
260
|
+
) -> AssistantMessage | None:
|
|
261
|
+
"""Convert an LLM output message dict to an AssistantMessage.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
msg: Raw LLM message dict from OpenInference attributes.
|
|
265
|
+
metadata: Per-span metadata (tokens, cost, latency) to attach.
|
|
266
|
+
"""
|
|
259
267
|
|
|
260
268
|
role = msg.get("role")
|
|
261
269
|
tool_calls = msg.get("tool_calls")
|
|
@@ -322,6 +330,7 @@ class AgentTrace(BaseModel):
|
|
|
322
330
|
return AssistantMessage(
|
|
323
331
|
content=text_content if text_content else None,
|
|
324
332
|
tool_calls=tool_calls_list if tool_calls_list else None,
|
|
333
|
+
metadata=metadata,
|
|
325
334
|
)
|
|
326
335
|
|
|
327
336
|
return None
|
|
@@ -336,6 +345,64 @@ class AgentTrace(BaseModel):
|
|
|
336
345
|
content=[MessageContentText(type="text", text=str(content))],
|
|
337
346
|
)
|
|
338
347
|
|
|
348
|
+
def _extract_assistant_metadata(self, span: AgentSpan) -> AssistantMessageMetadata:
|
|
349
|
+
"""Extract assistant message metadata from a span.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
span: The span to extract metadata from.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
AssistantMessageMetadata with tokens, cost, latency, and model info.
|
|
356
|
+
"""
|
|
357
|
+
# Per-span token and cost metrics for assistant messages
|
|
358
|
+
span_input_tokens = span.attributes.get(
|
|
359
|
+
SpanAttributes.LLM_TOKEN_COUNT_PROMPT, 0
|
|
360
|
+
)
|
|
361
|
+
span_output_tokens = span.attributes.get(
|
|
362
|
+
SpanAttributes.LLM_TOKEN_COUNT_COMPLETION, 0
|
|
363
|
+
)
|
|
364
|
+
span_input_cost = span.attributes.get(SpanAttributes.LLM_COST_PROMPT, 0.0)
|
|
365
|
+
span_output_cost = span.attributes.get(SpanAttributes.LLM_COST_COMPLETION, 0.0)
|
|
366
|
+
|
|
367
|
+
# Per-span latency (nanoseconds as float)
|
|
368
|
+
span_start_time = float(span.start_time) if span.start_time is not None else 0.0
|
|
369
|
+
span_end_time = float(span.end_time) if span.end_time is not None else 0.0
|
|
370
|
+
|
|
371
|
+
# Per-span model info
|
|
372
|
+
span_model_name = span.attributes.get(SpanAttributes.LLM_MODEL_NAME, "")
|
|
373
|
+
span_model_provider = span.attributes.get(SpanAttributes.LLM_PROVIDER, "")
|
|
374
|
+
|
|
375
|
+
return AssistantMessageMetadata(
|
|
376
|
+
tokens=MessageTokenInfo(
|
|
377
|
+
input_tokens=span_input_tokens,
|
|
378
|
+
output_tokens=span_output_tokens,
|
|
379
|
+
),
|
|
380
|
+
cost=MessageCostInfo(
|
|
381
|
+
input_cost=span_input_cost,
|
|
382
|
+
output_cost=span_output_cost,
|
|
383
|
+
),
|
|
384
|
+
latency=MessageLatencyInfo(
|
|
385
|
+
start_time=span_start_time,
|
|
386
|
+
end_time=span_end_time,
|
|
387
|
+
),
|
|
388
|
+
model_info=MessageModelInfo(
|
|
389
|
+
model_name=span_model_name,
|
|
390
|
+
model_provider=span_model_provider,
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def _extract_tool_metadata(self, span: AgentSpan) -> ToolMessageMetadata:
|
|
395
|
+
"""Extract tool message metadata from a span."""
|
|
396
|
+
span_start_time = float(span.start_time) if span.start_time is not None else 0.0
|
|
397
|
+
span_end_time = float(span.end_time) if span.end_time is not None else 0.0
|
|
398
|
+
|
|
399
|
+
return ToolMessageMetadata(
|
|
400
|
+
latency=MessageLatencyInfo(
|
|
401
|
+
start_time=span_start_time,
|
|
402
|
+
end_time=span_end_time,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
|
|
339
406
|
def to_agent_trajectory(
|
|
340
407
|
self,
|
|
341
408
|
framework: Framework = Framework.LANGGRAPH,
|
|
@@ -411,8 +478,10 @@ class AgentTrace(BaseModel):
|
|
|
411
478
|
if only_leaf_llms and span_id and has_llm_children(span_id):
|
|
412
479
|
continue
|
|
413
480
|
|
|
481
|
+
metadata = self._extract_assistant_metadata(span)
|
|
482
|
+
|
|
414
483
|
for llm_msg in span.to_llm_messages():
|
|
415
|
-
converted = self._convert_llm_message(llm_msg)
|
|
484
|
+
converted = self._convert_llm_message(llm_msg, metadata=metadata)
|
|
416
485
|
if converted:
|
|
417
486
|
messages.append(converted)
|
|
418
487
|
|
|
@@ -422,6 +491,7 @@ class AgentTrace(BaseModel):
|
|
|
422
491
|
if tool_msg:
|
|
423
492
|
converted = self._convert_tool_message(tool_msg)
|
|
424
493
|
if converted:
|
|
494
|
+
converted.metadata = self._extract_tool_metadata(span)
|
|
425
495
|
messages.append(converted)
|
|
426
496
|
|
|
427
497
|
return messages
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: quraite
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: This project provides adaptors and methods to integrate with the Quraite platform
|
|
5
|
+
Author: Shiv Mohith
|
|
6
|
+
Author-email: Shiv Mohith <shivmohith8@gmail.com>
|
|
7
|
+
Requires-Dist: aiohttp>=3.13.2
|
|
8
|
+
Requires-Dist: fastapi>=0.121.1
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
Requires-Dist: openinference-semantic-conventions>=0.1.25
|
|
11
|
+
Requires-Dist: opentelemetry-api>=1.37.0
|
|
12
|
+
Requires-Dist: opentelemetry-sdk>=1.37.0
|
|
13
|
+
Requires-Dist: pydantic>=2.12.4
|
|
14
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
15
|
+
Requires-Dist: uvicorn>=0.38.0
|
|
16
|
+
Requires-Dist: agno>=2.3.4 ; extra == 'agno'
|
|
17
|
+
Requires-Dist: boto3>=1.40.70 ; extra == 'bedrock'
|
|
18
|
+
Requires-Dist: google-adk>=1.18.0 ; extra == 'google-adk'
|
|
19
|
+
Requires-Dist: langchain>=1.0.5 ; extra == 'langgraph'
|
|
20
|
+
Requires-Dist: langgraph>=1.0.3 ; extra == 'langgraph'
|
|
21
|
+
Requires-Dist: openai-agents>=0.5.0 ; extra == 'openai-agents'
|
|
22
|
+
Requires-Dist: pydantic-ai>=1.25.0 ; extra == 'pydantic-ai'
|
|
23
|
+
Requires-Dist: pyngrok>=7.5.0 ; extra == 'pyngrok'
|
|
24
|
+
Requires-Dist: smolagents>=1.23.0 ; extra == 'smolagents'
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: agno
|
|
27
|
+
Provides-Extra: bedrock
|
|
28
|
+
Provides-Extra: google-adk
|
|
29
|
+
Provides-Extra: langgraph
|
|
30
|
+
Provides-Extra: openai-agents
|
|
31
|
+
Provides-Extra: pydantic-ai
|
|
32
|
+
Provides-Extra: pyngrok
|
|
33
|
+
Provides-Extra: smolagents
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Quraite Python SDK
|
|
37
|
+
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
The **Quraite Python SDK** provides adapters and methods to integrate AI agent frameworks with the [Quraite platform](https://quraite.ai). It offers a unified interface for different agent frameworks, automatic tracing and observability, and easy local server setup with tunneling capabilities.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- 🔌 **Framework Adapters**: Support for multiple AI agent frameworks (LangGraph, Pydantic AI, Agno, Google ADK, OpenAI Agents, Smolagents, AWS Bedrock, Flowise, Langflow, N8n, and more)
|
|
46
|
+
- 📊 **Automatic Tracing**: Built-in OpenTelemetry-based tracing for agent execution, tool calls, and performance metrics
|
|
47
|
+
- 🚀 **Local Server**: Easy-to-use local server with optional tunneling (Cloudflare/ngrok) for public access
|
|
48
|
+
- 📦 **Standardized Schema**: Unified message and response formats across all frameworks
|
|
49
|
+
- 🔍 **Observability**: Track token usage, costs, latency, and model information for each agent invocation
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### Basic Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install quraite
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Framework-Specific Installation
|
|
60
|
+
|
|
61
|
+
Install with optional dependencies for specific frameworks:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# LangGraph
|
|
65
|
+
pip install 'quraite[langgraph]'
|
|
66
|
+
|
|
67
|
+
# Pydantic AI
|
|
68
|
+
pip install 'quraite[pydantic-ai]'
|
|
69
|
+
|
|
70
|
+
# Agno
|
|
71
|
+
pip install 'quraite[agno]'
|
|
72
|
+
|
|
73
|
+
# Google ADK
|
|
74
|
+
pip install 'quraite[google-adk]'
|
|
75
|
+
|
|
76
|
+
# OpenAI Agents
|
|
77
|
+
pip install 'quraite[openai-agents]'
|
|
78
|
+
|
|
79
|
+
# Smolagents
|
|
80
|
+
pip install 'quraite[smolagents]'
|
|
81
|
+
|
|
82
|
+
# AWS Bedrock
|
|
83
|
+
pip install 'quraite[bedrock]'
|
|
84
|
+
|
|
85
|
+
# Multiple frameworks
|
|
86
|
+
pip install 'quraite[langgraph,pydantic-ai,agno]'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick Start
|
|
90
|
+
|
|
91
|
+
### Example: LangGraph Agent with Local Server
|
|
92
|
+
|
|
93
|
+
Pass your compiled LangGraph agent to the adapter and expose it as an HTTP API:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import uvicorn
|
|
97
|
+
from dotenv import load_dotenv
|
|
98
|
+
from openinference.instrumentation import TracerProvider
|
|
99
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
100
|
+
|
|
101
|
+
from quraite.adapters import LanggraphAdapter
|
|
102
|
+
from quraite.serve.local_agent import LocalAgentServer
|
|
103
|
+
from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
|
|
104
|
+
from quraite.tracing.span_processor import QuraiteSimpleSpanProcessor
|
|
105
|
+
|
|
106
|
+
load_dotenv()
|
|
107
|
+
|
|
108
|
+
# Set up tracing (optional)
|
|
109
|
+
tracer_provider = TracerProvider()
|
|
110
|
+
quraite_span_exporter = QuraiteInMemorySpanExporter()
|
|
111
|
+
quraite_span_processor = QuraiteSimpleSpanProcessor(quraite_span_exporter)
|
|
112
|
+
tracer_provider.add_span_processor(quraite_span_processor)
|
|
113
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
114
|
+
|
|
115
|
+
# Your compiled LangGraph agent (created elsewhere)
|
|
116
|
+
# agent = create_agent(...)
|
|
117
|
+
|
|
118
|
+
# Wrap with Quraite adapter
|
|
119
|
+
adapter = LanggraphAdapter(
|
|
120
|
+
agent_graph=agent, # Pass your compiled LangGraph agent here
|
|
121
|
+
tracer_provider=tracer_provider, # Optional: for tracing
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Create and start server with Cloudflare tunnel
|
|
125
|
+
server = LocalAgentServer(
|
|
126
|
+
wrapped_agent=adapter,
|
|
127
|
+
agent_id="your-agent-id", # Optional: for Quraite platform integration
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
app = server.create_app(
|
|
131
|
+
port=8080,
|
|
132
|
+
host="0.0.0.0",
|
|
133
|
+
tunnel="cloudflare", # Options: "cloudflare", "ngrok", or "none"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
uvicorn.run("local_server:app", host="0.0.0.0", port=8080)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The server exposes:
|
|
141
|
+
|
|
142
|
+
- `GET /` - Health check endpoint
|
|
143
|
+
- `POST /v1/agents/completions` - Agent invocation endpoint
|
|
144
|
+
|
|
145
|
+
When using `tunnel="cloudflare"` or `tunnel="ngrok"`, your agent will be publicly accessible via the generated URL.
|
|
146
|
+
|
|
147
|
+
## Supported Frameworks
|
|
148
|
+
|
|
149
|
+
| Framework | Adapter | Installation |
|
|
150
|
+
| -------------------- | ------------------------ | -------------------------------------- |
|
|
151
|
+
| **LangGraph** | `LanggraphAdapter` | `pip install 'quraite[langgraph]'` |
|
|
152
|
+
| **Pydantic AI** | `PydanticAIAdapter` | `pip install 'quraite[pydantic-ai]'` |
|
|
153
|
+
| **Agno** | `AgnoAdapter` | `pip install 'quraite[agno]'` |
|
|
154
|
+
| **Google ADK** | `GoogleADKAdapter` | `pip install 'quraite[google-adk]'` |
|
|
155
|
+
| **OpenAI Agents** | `OpenaiAgentsAdapter` | `pip install 'quraite[openai-agents]'` |
|
|
156
|
+
| **Smolagents** | `SmolagentsAdapter` | `pip install 'quraite[smolagents]'` |
|
|
157
|
+
| **AWS Bedrock** | `BedrockAgentsAdapter` | `pip install 'quraite[bedrock]'` |
|
|
158
|
+
| **Flowise** | `FlowiseAdapter` | Included in base package |
|
|
159
|
+
| **Langflow** | `LangflowAdapter` | Included in base package |
|
|
160
|
+
| **N8n** | `N8nAdapter` | Included in base package |
|
|
161
|
+
| **HTTP** | `HttpAdapter` | Included in base package |
|
|
162
|
+
| **LangGraph Server** | `LanggraphServerAdapter` | `pip install 'quraite[langgraph]'` |
|
|
163
|
+
|
|
164
|
+
## Core Concepts
|
|
165
|
+
|
|
166
|
+
### Adapters
|
|
167
|
+
|
|
168
|
+
Adapters provide a unified interface (`BaseAdapter`) for different agent frameworks. Each adapter:
|
|
169
|
+
|
|
170
|
+
- Converts framework-specific agents to a standard interface
|
|
171
|
+
- Handles message format conversion
|
|
172
|
+
- Supports optional tracing integration
|
|
173
|
+
- Provides async invocation via `ainvoke()`
|
|
174
|
+
|
|
175
|
+
### Tracing
|
|
176
|
+
|
|
177
|
+
The SDK includes built-in OpenTelemetry-based tracing that captures:
|
|
178
|
+
|
|
179
|
+
- **Agent Trajectory**: Complete conversation flow with all messages
|
|
180
|
+
- **Tool Calls**: Tool invocations with inputs and outputs
|
|
181
|
+
- **Performance Metrics**: Token usage, costs, latency
|
|
182
|
+
- **Model Information**: Model name and provider details
|
|
183
|
+
|
|
184
|
+
To enable tracing:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from openinference.instrumentation import TracerProvider
|
|
188
|
+
from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
|
|
189
|
+
from quraite.tracing.span_processor import QuraiteSimpleSpanProcessor
|
|
190
|
+
|
|
191
|
+
tracer_provider = TracerProvider()
|
|
192
|
+
quraite_span_exporter = QuraiteInMemorySpanExporter()
|
|
193
|
+
quraite_span_processor = QuraiteSimpleSpanProcessor(quraite_span_exporter)
|
|
194
|
+
tracer_provider.add_span_processor(quraite_span_processor)
|
|
195
|
+
|
|
196
|
+
# Instrument your framework (example for LangChain)
|
|
197
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
198
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Message Schema
|
|
202
|
+
|
|
203
|
+
The SDK uses a standardized message format:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from quraite.schema.message import (
|
|
207
|
+
UserMessage,
|
|
208
|
+
AssistantMessage,
|
|
209
|
+
ToolMessage,
|
|
210
|
+
SystemMessage,
|
|
211
|
+
MessageContentText,
|
|
212
|
+
ToolCall,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# User message
|
|
216
|
+
user_msg = UserMessage(
|
|
217
|
+
content=[MessageContentText(text="Hello, world!")]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Assistant message with tool calls
|
|
221
|
+
assistant_msg = AssistantMessage(
|
|
222
|
+
content=[MessageContentText(text="I'll calculate that for you.")],
|
|
223
|
+
tool_calls=[
|
|
224
|
+
ToolCall(
|
|
225
|
+
id="call_123",
|
|
226
|
+
name="add",
|
|
227
|
+
arguments={"a": 10, "b": 5}
|
|
228
|
+
)
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Tool message
|
|
233
|
+
tool_msg = ToolMessage(
|
|
234
|
+
tool_call_id="call_123",
|
|
235
|
+
content=[MessageContentText(text="15")]
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Response Format
|
|
240
|
+
|
|
241
|
+
Agent invocations return an `AgentInvocationResponse`:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from quraite.schema.response import AgentInvocationResponse
|
|
245
|
+
|
|
246
|
+
response: AgentInvocationResponse = await adapter.ainvoke(
|
|
247
|
+
input=[user_msg],
|
|
248
|
+
session_id="session-123"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Access trajectory (list of messages)
|
|
252
|
+
trajectory = response.agent_trajectory
|
|
253
|
+
|
|
254
|
+
# Access trace (if tracing enabled)
|
|
255
|
+
trace = response.agent_trace
|
|
256
|
+
|
|
257
|
+
# Access final response text
|
|
258
|
+
final_response = response.agent_final_response
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Examples
|
|
262
|
+
|
|
263
|
+
The repository includes comprehensive examples for each supported framework:
|
|
264
|
+
|
|
265
|
+
- [`langgraph_calculator_agent`](examples/langgraph_calculator_agent/) - LangGraph calculator agent
|
|
266
|
+
- [`pydantic_calculator_agent`](examples/pydantic_calculator_agent/) - Pydantic AI calculator agent
|
|
267
|
+
- [`agno_calculator_agent`](examples/agno_calculator_agent/) - Agno calculator agent
|
|
268
|
+
- [`google_adk_weather_agent`](examples/google_adk_weather_agent/) - Google ADK weather agent
|
|
269
|
+
- [`openai_flight_booking_agent`](examples/openai_flight_booking_agent/) - OpenAI Agents flight booking
|
|
270
|
+
- [`smolagents_sql_agent`](examples/smolagents_sql_agent/) - Smolagents SQL agent
|
|
271
|
+
- [`bedrock_restaurant_support_agent`](examples/bedrock_restaurant_support_agent/) - AWS Bedrock agent
|
|
272
|
+
- And more...
|
|
273
|
+
|
|
274
|
+
Each example includes:
|
|
275
|
+
|
|
276
|
+
- Agent implementation
|
|
277
|
+
- Adapter setup
|
|
278
|
+
- Local server configuration
|
|
279
|
+
- Environment variable examples
|
|
280
|
+
|
|
281
|
+
## API Reference
|
|
282
|
+
|
|
283
|
+
### BaseAdapter
|
|
284
|
+
|
|
285
|
+
All adapters inherit from `BaseAdapter`:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from quraite.adapters.base import BaseAdapter
|
|
289
|
+
|
|
290
|
+
class MyAdapter(BaseAdapter):
|
|
291
|
+
async def ainvoke(
|
|
292
|
+
self,
|
|
293
|
+
input: List[AgentMessage],
|
|
294
|
+
session_id: str | None,
|
|
295
|
+
) -> AgentInvocationResponse:
|
|
296
|
+
# Implementation
|
|
297
|
+
pass
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### LocalAgentServer
|
|
301
|
+
|
|
302
|
+
Create a local HTTP server for your agent:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from quraite.serve.local_agent import LocalAgentServer
|
|
306
|
+
|
|
307
|
+
server = LocalAgentServer(
|
|
308
|
+
wrapped_agent=adapter,
|
|
309
|
+
agent_id="optional-agent-id",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
app = server.create_app(
|
|
313
|
+
port=8080,
|
|
314
|
+
host="0.0.0.0",
|
|
315
|
+
tunnel="cloudflare", # or "ngrok" or "none"
|
|
316
|
+
)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Development
|
|
320
|
+
|
|
321
|
+
### Setup
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Clone the repository
|
|
325
|
+
git clone https://github.com/innowhyte/quraite-python.git
|
|
326
|
+
cd quraite-python
|
|
327
|
+
|
|
328
|
+
# Install dependencies
|
|
329
|
+
pip install -e ".[dev,test]"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Running Tests
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
pytest
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Building
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
make build
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Publishing
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Update version
|
|
348
|
+
make update-version v=0.4.0
|
|
349
|
+
|
|
350
|
+
# Build
|
|
351
|
+
make build
|
|
352
|
+
|
|
353
|
+
# Publish to Test PyPI
|
|
354
|
+
make publish
|
|
355
|
+
# Enter username as "__token__" and then enter your API key
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Requirements
|
|
359
|
+
|
|
360
|
+
- Python 3.10+
|
|
361
|
+
- See `pyproject.toml` for full dependency list
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
See [LICENSE](LICENSE) file for details.
|
|
366
|
+
|
|
367
|
+
## Contributing
|
|
368
|
+
|
|
369
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
370
|
+
|
|
371
|
+
## Support
|
|
372
|
+
|
|
373
|
+
For issues, questions, or contributions, please visit the [Quraite platform](https://quraite.ai) or open an issue on GitHub.
|
|
374
|
+
|
|
375
|
+
## Changelog
|
|
376
|
+
|
|
377
|
+
See the repository's commit history for detailed changes.
|
|
@@ -5,7 +5,7 @@ quraite/adapters/base.py,sha256=sgzXuCRt81LyH7kjP3UsccbgDbgVQeHKOfqcXah1mHo,3917
|
|
|
5
5
|
quraite/adapters/bedrock_agents_adapter.py,sha256=2G-zUXqhNSzpyxut2UQgqTfe01MWcdjt1ajZIb43CUM,14136
|
|
6
6
|
quraite/adapters/flowise_adapter.py,sha256=yLJ0tjDWMtTzm0bRYbhjfRm6wd5A6DqbHhqemy_FW48,10349
|
|
7
7
|
quraite/adapters/google_adk_adapter.py,sha256=Ci7L54gA6pEH3-qcZ0PW1qODV6flJsVVbL9sN78FT_Y,7390
|
|
8
|
-
quraite/adapters/http_adapter.py,sha256=
|
|
8
|
+
quraite/adapters/http_adapter.py,sha256=y5ft65fF6SfvJXkhDkoglvq6dv6_VlCWDTZSCLEgssE,7883
|
|
9
9
|
quraite/adapters/langflow_adapter.py,sha256=zGbDSd7Q_AZZ0ICByedsBIl5yNJ0hN6tQZgpUyN41-E,6946
|
|
10
10
|
quraite/adapters/langgraph_adapter.py,sha256=WqmbP6Zw1lwfSCBf_O7oKNoGEb_1-zw_PMS1v71i3-w,11603
|
|
11
11
|
quraite/adapters/langgraph_server_adapter.py,sha256=OhMx370Uzrvbp4yMOtgKkjQFI5u1xboKq0ybQRsBD90,9694
|
|
@@ -13,13 +13,13 @@ quraite/adapters/n8n_adapter.py,sha256=CCIOcZNxQw-69xdf9BVX9Jyt8i666AlsE2DJHsY8l
|
|
|
13
13
|
quraite/adapters/openai_agents_adapter.py,sha256=iCFOWrZ-HnLoWOB-xsyJZf37Nh5QW5j8sHNSkjSWdGc,9665
|
|
14
14
|
quraite/adapters/pydantic_ai_adapter.py,sha256=gps_90Zp8NcAiDX26-py4MKN2B5_jPEvgroRi8kjfQU,10811
|
|
15
15
|
quraite/adapters/smolagents_adapter.py,sha256=QhEyMDlFkkXPSc5PiQTxMA9i2fRitzNcMdEsZwe9QVY,5448
|
|
16
|
-
quraite/logger.py,sha256=
|
|
16
|
+
quraite/logger.py,sha256=Py5GRQfD_s5FVb2Ziz5JxJk9AT4ve6w_iePbOSQkwH4,1601
|
|
17
17
|
quraite/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
quraite/schema/message.py,sha256=
|
|
18
|
+
quraite/schema/message.py,sha256=D6HWaZJOcoKObzDD-vo4Qg-chJDyOSwC-9XiNv_0xFc,2416
|
|
19
19
|
quraite/schema/response.py,sha256=HUD_TyU6nibfXgRzdpD9LGoRtptD663oq7INy1sqaS4,412
|
|
20
20
|
quraite/serve/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
21
|
quraite/serve/cloudflared.py,sha256=7XR1tPl0E3Feawo-6VCM8SXfCAie4cw9T1mx-ZNVJKw,6482
|
|
22
|
-
quraite/serve/local_agent.py,sha256=
|
|
22
|
+
quraite/serve/local_agent.py,sha256=8-a_4AeHeK3nTsgoDJVcTx7s4YvPoUhd6Zu3KVWEVAQ,12845
|
|
23
23
|
quraite/traces/traces_adk_openinference.json,sha256=QjXAxMUEdSBcA5pCMOPiDU2_0NDsctEP6IFB6BIps9U,35957
|
|
24
24
|
quraite/traces/traces_agno_multi_agent.json,sha256=FEW_zkSCKYizKG40KNvfXuouXsmOTdsPC7nnXWaKLfE,65098
|
|
25
25
|
quraite/traces/traces_agno_openinference.json,sha256=p_tyBAp9KaT23PqGBU1cSMli7By0vss9Vpm3tL4pTMM,36871
|
|
@@ -39,11 +39,11 @@ quraite/tracing/constants.py,sha256=KhBIHkHRXAGW6xqa9vFa1LnA94C-EUTrWh5zNsx_z4Y,
|
|
|
39
39
|
quraite/tracing/span_exporter.py,sha256=0S3qyPgtPfZtb-IhEoQKm0ciFZoHvlaFUcqbNtDh7kM,4676
|
|
40
40
|
quraite/tracing/span_processor.py,sha256=mp5AR5RbRz1ddgJEK07CbXRL8NfaFj2DXP1bs9xcf4w,1603
|
|
41
41
|
quraite/tracing/tool_extractors.py,sha256=BXTr0lT570jSlQtwLvFGarEvBSkvAs07YSo6Ah9Qkkw,9232
|
|
42
|
-
quraite/tracing/trace.py,sha256=
|
|
42
|
+
quraite/tracing/trace.py,sha256=bLIXyk8XXRC6LcPImDnwowW20eg2WI6qUW6-SFrTLpQ,21646
|
|
43
43
|
quraite/tracing/types.py,sha256=sWyp-7Qc8zsWN6ltem8OSTRIgKWbqnQSfsTKX6RrrB8,4948
|
|
44
44
|
quraite/tracing/utils.py,sha256=Vm5q_iN_K1f_nEMpkQzv6jiUPpS2Yzo_VP5ZVmJZdBQ,5673
|
|
45
45
|
quraite/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
quraite/utils/json_utils.py,sha256=RewoObREfHnLBVI6MBCIEov6CaIxs8d67jbKEH79mQ0,18686
|
|
47
|
-
quraite-0.
|
|
48
|
-
quraite-0.
|
|
49
|
-
quraite-0.
|
|
47
|
+
quraite-0.1.1.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
|
|
48
|
+
quraite-0.1.1.dist-info/METADATA,sha256=av4RLcrkIZpgsYRyY3IZ1G8opLtP_6QBOBM-ft-p64M,11163
|
|
49
|
+
quraite-0.1.1.dist-info/RECORD,,
|
quraite-0.0.2.dist-info/METADATA
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: quraite
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: This project provides adaptors and methods to integrate with the Quraite platform
|
|
5
|
-
Author: Shiv Mohith
|
|
6
|
-
Author-email: Shiv Mohith <shivmohith8@gmail.com>
|
|
7
|
-
Requires-Dist: aiohttp>=3.13.2
|
|
8
|
-
Requires-Dist: fastapi>=0.121.1
|
|
9
|
-
Requires-Dist: httpx>=0.28.1
|
|
10
|
-
Requires-Dist: openinference-semantic-conventions>=0.1.25
|
|
11
|
-
Requires-Dist: opentelemetry-api>=1.37.0
|
|
12
|
-
Requires-Dist: opentelemetry-sdk>=1.37.0
|
|
13
|
-
Requires-Dist: pydantic>=2.12.4
|
|
14
|
-
Requires-Dist: python-dotenv>=1.2.1
|
|
15
|
-
Requires-Dist: uvicorn>=0.38.0
|
|
16
|
-
Requires-Dist: agno>=2.3.4 ; extra == 'agno'
|
|
17
|
-
Requires-Dist: boto3>=1.40.70 ; extra == 'bedrock'
|
|
18
|
-
Requires-Dist: google-adk>=1.18.0 ; extra == 'google-adk'
|
|
19
|
-
Requires-Dist: langchain>=1.0.5 ; extra == 'langgraph'
|
|
20
|
-
Requires-Dist: langgraph>=1.0.3 ; extra == 'langgraph'
|
|
21
|
-
Requires-Dist: openai-agents>=0.5.0 ; extra == 'openai-agents'
|
|
22
|
-
Requires-Dist: pydantic-ai>=1.25.0 ; extra == 'pydantic-ai'
|
|
23
|
-
Requires-Dist: pyngrok>=7.5.0 ; extra == 'pyngrok'
|
|
24
|
-
Requires-Dist: smolagents>=1.23.0 ; extra == 'smolagents'
|
|
25
|
-
Requires-Python: >=3.10
|
|
26
|
-
Provides-Extra: agno
|
|
27
|
-
Provides-Extra: bedrock
|
|
28
|
-
Provides-Extra: google-adk
|
|
29
|
-
Provides-Extra: langgraph
|
|
30
|
-
Provides-Extra: openai-agents
|
|
31
|
-
Provides-Extra: pydantic-ai
|
|
32
|
-
Provides-Extra: pyngrok
|
|
33
|
-
Provides-Extra: smolagents
|
|
34
|
-
Description-Content-Type: text/markdown
|
|
35
|
-
|
|
36
|
-
## Quraite Python SDK
|
|
37
|
-
|
|
38
|
-
### Publishing to Test PyPI
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
make update-version v=0.4.0
|
|
42
|
-
make build
|
|
43
|
-
make publish-test-pypi # enter user name as "__token__" and then enter the key
|
|
44
|
-
```
|
|
File without changes
|