quraite 0.1.1__py3-none-any.whl → 0.1.3__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.
@@ -8,9 +8,9 @@ if TYPE_CHECKING:
8
8
  from quraite.adapters.bedrock_agents_adapter import BedrockAgentsAdapter
9
9
  from quraite.adapters.flowise_adapter import FlowiseAdapter
10
10
  from quraite.adapters.google_adk_adapter import GoogleADKAdapter
11
+ from quraite.adapters.langchain_adapter import LangchainAdapter
12
+ from quraite.adapters.langchain_server_adapter import LangchainServerAdapter
11
13
  from quraite.adapters.langflow_adapter import LangflowAdapter
12
- from quraite.adapters.langgraph_adapter import LanggraphAdapter
13
- from quraite.adapters.langgraph_server_adapter import LanggraphServerAdapter
14
14
  from quraite.adapters.n8n_adapter import N8nAdapter
15
15
  from quraite.adapters.openai_agents_adapter import OpenaiAgentsAdapter
16
16
  from quraite.adapters.pydantic_ai_adapter import PydanticAIAdapter
@@ -25,8 +25,8 @@ __all__ = [
25
25
  "FlowiseAdapter",
26
26
  "GoogleADKAdapter",
27
27
  "LangflowAdapter",
28
- "LanggraphAdapter",
29
- "LanggraphServerAdapter",
28
+ "LangchainAdapter",
29
+ "LangchainServerAdapter",
30
30
  "N8nAdapter",
31
31
  "OpenaiAgentsAdapter",
32
32
  "PydanticAIAdapter",
@@ -76,24 +76,24 @@ def __getattr__(name: str):
76
76
 
77
77
  return LangflowAdapter
78
78
 
79
- elif name == "LanggraphAdapter":
79
+ elif name == "LangchainAdapter":
80
80
  try:
81
- from quraite.adapters.langgraph_adapter import LanggraphAdapter
81
+ from quraite.adapters.langchain_adapter import LangchainAdapter
82
82
 
83
- return LanggraphAdapter
83
+ return LangchainAdapter
84
84
  except ImportError as e:
85
85
  raise ImportError(
86
- f"Failed to import {name}. Please install the 'langgraph' optional dependency: pip install 'quraite[langgraph]'"
86
+ f"Failed to import {name}. Please install the 'langchain' optional dependency: pip install 'quraite[langchain]'"
87
87
  ) from e
88
88
 
89
- elif name == "LanggraphServerAdapter":
89
+ elif name == "LangchainServerAdapter":
90
90
  try:
91
- from quraite.adapters.langgraph_server_adapter import LanggraphServerAdapter
91
+ from quraite.adapters.langchain_server_adapter import LangchainServerAdapter
92
92
 
93
- return LanggraphServerAdapter
93
+ return LangchainServerAdapter
94
94
  except ImportError as e:
95
95
  raise ImportError(
96
- f"Failed to import {name}. Please install the 'langgraph' optional dependency: pip install 'quraite[langgraph]'"
96
+ f"Failed to import {name}. Please install the 'langchain' optional dependency: pip install 'quraite[langchain]'"
97
97
  ) from e
98
98
 
99
99
  elif name == "N8nAdapter":
@@ -1,4 +1,3 @@
1
- import uuid
2
1
  from typing import List, Union
3
2
 
4
3
  from agno.agent import Agent
@@ -9,7 +8,7 @@ from quraite.adapters.base import BaseAdapter
9
8
  from quraite.logger import get_logger
10
9
  from quraite.schema.message import AgentMessage
11
10
  from quraite.schema.response import AgentInvocationResponse
12
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
11
+ from quraite.tracing.constants import Framework
13
12
  from quraite.tracing.trace import AgentSpan, AgentTrace
14
13
 
15
14
  logger = get_logger(__name__)
@@ -122,27 +121,26 @@ class AgnoAdapter(BaseAdapter):
122
121
  session_id: str,
123
122
  ) -> AgentInvocationResponse:
124
123
  """Execute ainvoke with tracing enabled."""
125
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid.uuid4()}"
126
- logger.debug(
127
- "Starting traced invocation (trace_id=%s, session_id=%s)",
128
- adapter_trace_id,
129
- session_id,
130
- )
131
-
132
- with self.tracer.start_as_current_span(name=adapter_trace_id):
124
+ with self.tracer.start_as_current_span("agno_invocation") as span:
125
+ trace_id = span.get_span_context().trace_id
126
+ logger.debug(
127
+ "Starting traced invocation (session_id=%s) with trace_id=%s",
128
+ session_id,
129
+ trace_id,
130
+ )
133
131
  # Run the agent/team
134
132
  await self.agent.arun(agent_input, session_id=session_id)
135
133
 
136
134
  # Get trace spans
137
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
138
- adapter_trace_id
135
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
136
+ trace_id
139
137
  )
140
138
 
141
139
  if trace_readable_spans:
142
140
  logger.info(
143
141
  "Retrieved %d spans for trace_id=%s",
144
142
  len(trace_readable_spans),
145
- adapter_trace_id,
143
+ trace_id,
146
144
  )
147
145
  agent_trace = AgentTrace(
148
146
  spans=[
@@ -151,7 +149,7 @@ class AgnoAdapter(BaseAdapter):
151
149
  ],
152
150
  )
153
151
  else:
154
- logger.warning("No spans found for trace_id=%s", adapter_trace_id)
152
+ logger.warning("No spans found for trace_id=%s", trace_id)
155
153
 
156
154
  return AgentInvocationResponse(
157
155
  agent_trace=agent_trace,
quraite/adapters/base.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Any, List, Optional, Union
3
3
 
4
- from opentelemetry.trace import TracerProvider
4
+ from opentelemetry.trace import Tracer, TracerProvider
5
5
 
6
6
  from quraite.schema.message import AgentMessage, AssistantMessage, MessageContentText
7
7
  from quraite.schema.response import AgentInvocationResponse
@@ -22,7 +22,7 @@ class BaseAdapter(ABC):
22
22
  """
23
23
 
24
24
  tracer_provider: Optional[TracerProvider] = None
25
- tracer: Optional[Any] = None
25
+ tracer: Optional[Tracer] = None
26
26
  quraite_span_exporter: Optional[QuraiteInMemorySpanExporter] = None
27
27
 
28
28
  def _init_tracing(
@@ -13,7 +13,7 @@ from quraite.adapters.base import BaseAdapter
13
13
  from quraite.logger import get_logger
14
14
  from quraite.schema.message import AgentMessage
15
15
  from quraite.schema.response import AgentInvocationResponse
16
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
16
+ from quraite.tracing.constants import Framework
17
17
  from quraite.tracing.trace import AgentSpan, AgentTrace
18
18
 
19
19
  logger = get_logger(__name__)
@@ -141,14 +141,18 @@ class GoogleADKAdapter(BaseAdapter):
141
141
  session_id: str,
142
142
  ) -> AgentInvocationResponse:
143
143
  """Execute ainvoke with tracing enabled."""
144
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid.uuid4()}"
145
144
  logger.debug(
146
- "Starting Google ADK traced invocation (trace_id=%s, session_id=%s)",
147
- adapter_trace_id,
145
+ "Starting Google ADK traced invocation (session_id=%s)",
148
146
  session_id,
149
147
  )
150
148
 
151
- with self.tracer.start_as_current_span(name=adapter_trace_id):
149
+ with self.tracer.start_as_current_span("google_adk_invocation") as span:
150
+ trace_id = span.get_span_context().trace_id
151
+ logger.debug(
152
+ "Starting Google ADK traced invocation (trace_id=%s, session_id=%s)",
153
+ trace_id,
154
+ session_id,
155
+ )
152
156
  # Create session if it doesn't exist
153
157
  try:
154
158
  await self.session_service.create_session(
@@ -180,8 +184,8 @@ class GoogleADKAdapter(BaseAdapter):
180
184
  pass # Just consume events, tracing handles capture
181
185
 
182
186
  # Get trace spans
183
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
184
- adapter_trace_id
187
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
188
+ trace_id
185
189
  )
186
190
 
187
191
  if trace_readable_spans:
@@ -194,12 +198,10 @@ class GoogleADKAdapter(BaseAdapter):
194
198
  logger.info(
195
199
  "Google ADK trace collected %d spans for trace_id=%s",
196
200
  len(trace_readable_spans),
197
- adapter_trace_id,
201
+ trace_id,
198
202
  )
199
203
  else:
200
- logger.warning(
201
- "No spans exported for Google ADK trace_id=%s", adapter_trace_id
202
- )
204
+ logger.warning("No spans exported for Google ADK trace_id=%s", trace_id)
203
205
 
204
206
  return AgentInvocationResponse(
205
207
  agent_trace=agent_trace,
@@ -239,10 +239,14 @@ if __name__ == "__main__":
239
239
 
240
240
  try:
241
241
  response = await adapter.ainvoke(
242
- input=[UserMessage(content=[MessageContentText(text="What is 1 + 1")])],
242
+ input=[
243
+ UserMessage(
244
+ content=[MessageContentText(text="What is 34354 - 54?")]
245
+ )
246
+ ],
243
247
  session_id="test",
244
248
  )
245
- print(response)
249
+ print(response.agent_trajectory)
246
250
  except httpx.HTTPStatusError as e:
247
251
  print(json.loads(e.response.text)["detail"])
248
252
  except Exception as e:
@@ -1,5 +1,4 @@
1
1
  import json
2
- import uuid
3
2
  from typing import Optional
4
3
 
5
4
  from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
@@ -14,7 +13,7 @@ from quraite.schema.message import ToolCall
14
13
  from quraite.schema.message import ToolMessage as QuraiteToolMessage
15
14
  from quraite.schema.message import UserMessage
16
15
  from quraite.schema.response import AgentInvocationResponse
17
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
16
+ from quraite.tracing.constants import Framework
18
17
  from quraite.tracing.trace import AgentSpan, AgentTrace
19
18
 
20
19
  LangchainMessage = HumanMessage | SystemMessage | AIMessage | ToolMessage
@@ -22,12 +21,12 @@ LangchainMessage = HumanMessage | SystemMessage | AIMessage | ToolMessage
22
21
  logger = get_logger(__name__)
23
22
 
24
23
 
25
- class LanggraphAdapter(BaseAdapter):
24
+ class LangchainAdapter(BaseAdapter):
26
25
  """
27
- LangGraph adapter wrapper that converts any LangGraph agent
26
+ LangChain adapter wrapper that converts any LangChain agent
28
27
  to a standardized callable interface (invoke) and converts the output to List[AgentMessage].
29
28
 
30
- This class wraps any LangGraph CompiledGraph and provides:
29
+ This class wraps any LangChain CompiledGraph and provides:
31
30
  - Synchronous invocation via invoke()
32
31
  - Asynchronous invocation via ainvoke()
33
32
  - Automatic conversion to List[AgentMessage] format
@@ -36,22 +35,22 @@ class LanggraphAdapter(BaseAdapter):
36
35
  def __init__(
37
36
  self,
38
37
  agent_graph: CompiledStateGraph,
39
- agent_name: str = "LangGraph Agent",
38
+ agent_name: str = "LangChain Agent",
40
39
  tracer_provider: Optional[TracerProvider] = None,
41
40
  ):
42
41
  """
43
- Initialize with a pre-configured LangGraph agent
42
+ Initialize with a pre-configured LangChain agent
44
43
 
45
44
  Args:
46
- agent_graph: Any CompiledGraph from LangGraph (must have invoke/ainvoke methods)
45
+ agent_graph: Any CompiledGraph from LangChain (must have invoke/ainvoke methods)
47
46
  agent_name: Name of the agent for trajectory metadata
48
47
  """
49
- logger.debug("Initializing LanggraphAdapter with agent_name=%s", agent_name)
48
+ logger.debug("Initializing LangchainAdapter with agent_name=%s", agent_name)
50
49
  self.agent_graph = agent_graph
51
50
  self.agent_name = agent_name
52
51
  self._init_tracing(tracer_provider, required=False)
53
52
  logger.info(
54
- "LanggraphAdapter initialized successfully (tracing_enabled=%s)",
53
+ "LangchainAdapter initialized successfully (tracing_enabled=%s)",
55
54
  bool(tracer_provider),
56
55
  )
57
56
 
@@ -59,7 +58,7 @@ class LanggraphAdapter(BaseAdapter):
59
58
  self, input: list[AgentMessage]
60
59
  ) -> dict[str, list[HumanMessage]]:
61
60
  """
62
- Prepare input for LangGraph agent from List[Message].
61
+ Prepare input for LangChain agent from List[Message].
63
62
 
64
63
  Args:
65
64
  input: List[AgentMessage] containing user_message
@@ -201,7 +200,7 @@ class LanggraphAdapter(BaseAdapter):
201
200
  session_id: str | None,
202
201
  ) -> AgentInvocationResponse:
203
202
  """
204
- Asynchronous invocation method - invokes the LangGraph agent and converts the output to List[AgentMessage]
203
+ Asynchronous invocation method - invokes the LangChain agent and converts the output to List[AgentMessage]
205
204
 
206
205
  Args:
207
206
  input: List[AgentMessage] containing user_message
@@ -238,14 +237,16 @@ class LanggraphAdapter(BaseAdapter):
238
237
  config: dict,
239
238
  ) -> AgentInvocationResponse:
240
239
  """Execute ainvoke with tracing enabled."""
241
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid.uuid4()}"
242
- logger.debug("Starting traced invocation (trace_id=%s)", adapter_trace_id)
243
-
244
- with self.tracer.start_as_current_span(name=adapter_trace_id):
245
- result = await self.agent_graph.ainvoke(agent_input, config=config)
240
+ with self.tracer.start_as_current_span("langchain_invocation") as span:
241
+ trace_id = span.get_span_context().trace_id
242
+ logger.debug(
243
+ "Starting LangChain traced invocation (trace_id=%s)",
244
+ trace_id,
245
+ )
246
+ _ = await self.agent_graph.ainvoke(agent_input, config=config)
246
247
 
247
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
248
- adapter_trace_id
248
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
249
+ trace_id
249
250
  )
250
251
 
251
252
  if trace_readable_spans:
@@ -257,7 +258,7 @@ class LanggraphAdapter(BaseAdapter):
257
258
  ]
258
259
  )
259
260
 
260
- trajectory = agent_trace.to_agent_trajectory(framework=Framework.LANGGRAPH)
261
+ trajectory = agent_trace.to_agent_trajectory(framework=Framework.LANGCHAIN)
261
262
  logger.debug("Generated trajectory with %d messages", len(trajectory))
262
263
 
263
264
  return AgentInvocationResponse(
@@ -265,7 +266,7 @@ class LanggraphAdapter(BaseAdapter):
265
266
  agent_trajectory=trajectory,
266
267
  )
267
268
 
268
- logger.warning("No trace spans found for trace_id=%s", adapter_trace_id)
269
+ logger.warning("No trace spans found for trace_id=%s", trace_id)
269
270
  return AgentInvocationResponse()
270
271
 
271
272
  async def _ainvoke_without_tracing(
@@ -22,11 +22,11 @@ LangchainMessage = Annotated[
22
22
  logger = get_logger(__name__)
23
23
 
24
24
 
25
- class LanggraphServerAdapter(BaseAdapter):
26
- """Remote LangGraph server adapter based on langgraph-sdk.
25
+ class LangchainServerAdapter(BaseAdapter):
26
+ """Remote LangChain server adapter based on langgraph-sdk.
27
27
 
28
28
  Args:
29
- base_url: The base URL of the LangGraph server
29
+ base_url: The base URL of the LangChain server
30
30
  assistant_id: The ID of the assistant to invoke
31
31
  **kwargs: Additional keyword arguments passed directly to
32
32
  langgraph_sdk.get_client() and get_sync_client().
@@ -49,7 +49,7 @@ class LanggraphServerAdapter(BaseAdapter):
49
49
  self.graph_name = graph_name
50
50
 
51
51
  logger.debug(
52
- "Initializing LanggraphServerAdapter (base_url=%s, assistant_id=%s, graph_name=%s)",
52
+ "Initializing LangchainServerAdapter (base_url=%s, assistant_id=%s, graph_name=%s)",
53
53
  base_url,
54
54
  assistant_id,
55
55
  graph_name,
@@ -73,17 +73,17 @@ class LanggraphServerAdapter(BaseAdapter):
73
73
  )
74
74
  except Exception as exc:
75
75
  raise RuntimeError(
76
- f"Failed to initialize LangGraph RemoteGraph for {self.base_url}: {exc}"
76
+ f"Failed to initialize LangChain RemoteGraph for {self.base_url}: {exc}"
77
77
  )
78
78
  logger.info(
79
- "LanggraphServerAdapter initialized (assistant_id=%s, graph_name=%s)",
79
+ "LangchainServerAdapter initialized (assistant_id=%s, graph_name=%s)",
80
80
  self.assistant_id,
81
81
  self.graph_name,
82
82
  )
83
83
 
84
84
  def _prepare_input(self, input: List[AgentMessage]) -> Any:
85
85
  """
86
- Prepare input for LangGraph agent from List[AgentMessage].
86
+ Prepare input for LangChain agent from List[AgentMessage].
87
87
 
88
88
  Args:
89
89
  input: List[AgentMessage] containing user_message
@@ -91,15 +91,15 @@ class LanggraphServerAdapter(BaseAdapter):
91
91
  Returns:
92
92
  Dict with messages list containing user_message
93
93
  """
94
- logger.debug("Preparing Langgraph server input from %d messages", len(input))
94
+ logger.debug("Preparing Langchain server input from %d messages", len(input))
95
95
  if not input or input[-1].role != "user":
96
- logger.error("Langgraph server input missing user message")
96
+ logger.error("Langchain server input missing user message")
97
97
  raise ValueError("No user message found in the input")
98
98
 
99
99
  last_user_message = input[-1]
100
100
  # Check if content list is not empty and has text
101
101
  if not last_user_message.content:
102
- logger.error("Langgraph server user message missing content")
102
+ logger.error("Langchain server user message missing content")
103
103
  raise ValueError("User message has no content")
104
104
 
105
105
  # Find the first text content item
@@ -110,11 +110,11 @@ class LanggraphServerAdapter(BaseAdapter):
110
110
  break
111
111
 
112
112
  if not text_content:
113
- logger.error("Langgraph server user message missing text content")
113
+ logger.error("Langchain server user message missing text content")
114
114
  raise ValueError("No text content found in user message")
115
115
 
116
116
  logger.debug(
117
- "Prepared Langgraph server input (text_length=%d)", len(text_content)
117
+ "Prepared Langchain server input (text_length=%d)", len(text_content)
118
118
  )
119
119
  return {"messages": [HumanMessage(content=text_content).model_dump()]}
120
120
 
@@ -123,7 +123,7 @@ class LanggraphServerAdapter(BaseAdapter):
123
123
  messages: List[dict],
124
124
  ) -> List[AgentMessage]:
125
125
  logger.debug(
126
- "Converting %d Langgraph server messages to quraite format", len(messages)
126
+ "Converting %d Langchain server messages to quraite format", len(messages)
127
127
  )
128
128
  converted_messages: List[AgentMessage] = []
129
129
 
@@ -201,7 +201,7 @@ class LanggraphServerAdapter(BaseAdapter):
201
201
  continue
202
202
 
203
203
  logger.info(
204
- "Langgraph server message conversion produced %d messages",
204
+ "Langchain server message conversion produced %d messages",
205
205
  len(converted_messages),
206
206
  )
207
207
  return converted_messages
@@ -209,7 +209,7 @@ class LanggraphServerAdapter(BaseAdapter):
209
209
  async def ainvoke(
210
210
  self,
211
211
  input: List[AgentMessage],
212
- session_id: Annotated[Union[str, None], "Thread ID used by LangGraph API"],
212
+ session_id: Annotated[Union[str, None], "Thread ID used by LangChain API"],
213
213
  ) -> AgentInvocationResponse:
214
214
  agent_messages = []
215
215
  agent_input = self._prepare_input(input)
@@ -219,19 +219,19 @@ class LanggraphServerAdapter(BaseAdapter):
219
219
  config = {}
220
220
 
221
221
  try:
222
- logger.info("Langgraph server ainvoke called (session_id=%s)", session_id)
222
+ logger.info("Langchain server ainvoke called (session_id=%s)", session_id)
223
223
  async for event in self.remote_graph.astream(agent_input, config=config):
224
224
  for _, result in event.items():
225
225
  if result.get("messages"):
226
226
  logger.debug(
227
- "Langgraph server received %d messages from stream chunk",
227
+ "Langchain server received %d messages from stream chunk",
228
228
  len(result.get("messages")),
229
229
  )
230
230
  agent_messages += result.get("messages")
231
231
 
232
232
  except Exception as e:
233
- logger.exception("Error invoking Langgraph remote graph")
234
- raise RuntimeError(f"Error invoking LangGraph agent: {e}") from e
233
+ logger.exception("Error invoking Langchain remote graph")
234
+ raise RuntimeError(f"Error invoking LangChain agent: {e}") from e
235
235
 
236
236
  try:
237
237
  # Convert to List[AgentMessage]
@@ -239,7 +239,7 @@ class LanggraphServerAdapter(BaseAdapter):
239
239
  agent_messages
240
240
  )
241
241
  logger.info(
242
- "Langgraph server ainvoke produced %d trajectory messages",
242
+ "Langchain server ainvoke produced %d trajectory messages",
243
243
  len(agent_trajectory),
244
244
  )
245
245
 
@@ -248,5 +248,5 @@ class LanggraphServerAdapter(BaseAdapter):
248
248
  )
249
249
 
250
250
  except ValueError:
251
- logger.exception("Langgraph server conversion to AgentMessage failed")
251
+ logger.exception("Langchain server conversion to AgentMessage failed")
252
252
  return AgentInvocationResponse()
@@ -1,5 +1,4 @@
1
1
  import json
2
- import uuid
3
2
  from typing import Dict, List, Optional, Union
4
3
 
5
4
  from agents import (
@@ -27,7 +26,7 @@ from quraite.schema.message import (
27
26
  ToolMessage,
28
27
  )
29
28
  from quraite.schema.response import AgentInvocationResponse
30
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
29
+ from quraite.tracing.constants import Framework
31
30
  from quraite.tracing.trace import AgentSpan, AgentTrace
32
31
 
33
32
  logger = get_logger(__name__)
@@ -200,22 +199,21 @@ class OpenaiAgentsAdapter(BaseAdapter):
200
199
  session: Session,
201
200
  ) -> AgentInvocationResponse:
202
201
  """Execute ainvoke with tracing enabled."""
203
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid.uuid4()}"
204
- logger.debug(
205
- "Starting OpenAI traced invocation (trace_id=%s, session_id=%s)",
206
- adapter_trace_id,
207
- session.session_id if session else None,
208
- )
209
-
210
- with self.tracer.start_as_current_span(name=adapter_trace_id):
202
+ with self.tracer.start_as_current_span("openai_invocation") as span:
203
+ trace_id = span.get_span_context().trace_id
204
+ logger.debug(
205
+ "Starting OpenAI traced invocation (trace_id=%s session_id=%s)",
206
+ trace_id,
207
+ session.session_id if session else None,
208
+ )
211
209
  await Runner.run(
212
210
  self.agent,
213
211
  input=agent_input,
214
212
  session=session,
215
213
  )
216
214
 
217
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
218
- adapter_trace_id
215
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
216
+ trace_id
219
217
  )
220
218
 
221
219
  if trace_readable_spans:
@@ -228,12 +226,12 @@ class OpenaiAgentsAdapter(BaseAdapter):
228
226
  logger.info(
229
227
  "OpenAI trace collected %d spans for trace_id=%s",
230
228
  len(trace_readable_spans),
231
- adapter_trace_id,
229
+ trace_id,
232
230
  )
233
231
  else:
234
232
  logger.warning(
235
233
  "No spans exported for OpenAI trace_id=%s",
236
- adapter_trace_id,
234
+ trace_id,
237
235
  )
238
236
 
239
237
  return AgentInvocationResponse(
@@ -21,7 +21,7 @@ from quraite.schema.message import (
21
21
  ToolMessage,
22
22
  )
23
23
  from quraite.schema.response import AgentInvocationResponse
24
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
24
+ from quraite.tracing.constants import Framework
25
25
  from quraite.tracing.trace import AgentSpan, AgentTrace
26
26
 
27
27
  logger = get_logger(__name__)
@@ -228,16 +228,13 @@ class PydanticAIAdapter(BaseAdapter):
228
228
  message_history: Any | None,
229
229
  ) -> AgentInvocationResponse:
230
230
  """Execute ainvoke with tracing enabled."""
231
- from uuid import uuid4
232
-
233
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid4()}"
234
- logger.debug(
235
- "Starting Pydantic AI traced invocation (trace_id=%s, session_id=%s)",
236
- adapter_trace_id,
237
- session_id,
238
- )
239
-
240
- with self.tracer.start_as_current_span(name=adapter_trace_id):
231
+ with self.tracer.start_as_current_span("pydantic_invocation") as span:
232
+ trace_id = span.get_span_context().trace_id
233
+ logger.debug(
234
+ "Starting Pydantic AI traced invocation (trace_id=%s session_id=%s)",
235
+ trace_id,
236
+ session_id,
237
+ )
241
238
  # Run the agent asynchronously with message history for context
242
239
  if message_history:
243
240
  result = await self.agent.run(
@@ -250,8 +247,8 @@ class PydanticAIAdapter(BaseAdapter):
250
247
  # Store the complete updated message history for this session
251
248
  self._sessions[session_id] = result.all_messages()
252
249
 
253
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
254
- adapter_trace_id
250
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
251
+ trace_id
255
252
  )
256
253
 
257
254
  if trace_readable_spans:
@@ -264,12 +261,10 @@ class PydanticAIAdapter(BaseAdapter):
264
261
  logger.info(
265
262
  "Pydantic AI trace collected %d spans for trace_id=%s",
266
263
  len(trace_readable_spans),
267
- adapter_trace_id,
264
+ trace_id,
268
265
  )
269
266
  else:
270
- logger.warning(
271
- "No spans exported for Pydantic AI trace_id=%s", adapter_trace_id
272
- )
267
+ logger.warning("No spans exported for Pydantic AI trace_id=%s", trace_id)
273
268
 
274
269
  return AgentInvocationResponse(
275
270
  agent_trace=agent_trace,
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import uuid
3
2
  from typing import List, Optional
4
3
 
5
4
  from opentelemetry.trace import TracerProvider
@@ -9,7 +8,7 @@ from quraite.adapters.base import BaseAdapter
9
8
  from quraite.logger import get_logger
10
9
  from quraite.schema.message import AgentMessage
11
10
  from quraite.schema.response import AgentInvocationResponse
12
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX, Framework
11
+ from quraite.tracing.constants import Framework
13
12
  from quraite.tracing.trace import AgentSpan, AgentTrace
14
13
 
15
14
  logger = get_logger(__name__)
@@ -114,24 +113,23 @@ class SmolagentsAdapter(BaseAdapter):
114
113
 
115
114
  async def _ainvoke_with_tracing(self, agent_input: str) -> AgentInvocationResponse:
116
115
  """Execute ainvoke with tracing enabled."""
117
- adapter_trace_id = f"{QURAITE_ADAPTER_TRACE_PREFIX}-{uuid.uuid4()}"
118
- logger.debug(
119
- "Starting Smolagents traced invocation (trace_id=%s)", adapter_trace_id
120
- )
121
-
122
- with self.tracer.start_as_current_span(name=adapter_trace_id):
116
+ with self.tracer.start_as_current_span("smolagents_invocation") as span:
117
+ trace_id = span.get_span_context().trace_id
118
+ logger.debug(
119
+ "Starting Smolagents traced invocation (trace_id=%s)", trace_id
120
+ )
123
121
  # Run the agent synchronously inside a thread to avoid blocking
124
122
  await asyncio.to_thread(self.agent.run, agent_input)
125
123
 
126
- trace_readable_spans = self.quraite_span_exporter.get_trace_by_testcase(
127
- adapter_trace_id
124
+ trace_readable_spans = self.quraite_span_exporter.get_spans_by_trace_id(
125
+ trace_id
128
126
  )
129
127
 
130
128
  if trace_readable_spans:
131
129
  logger.info(
132
130
  "Smolagents trace collected %d spans for trace_id=%s",
133
131
  len(trace_readable_spans),
134
- adapter_trace_id,
132
+ trace_id,
135
133
  )
136
134
  agent_trace = AgentTrace(
137
135
  spans=[
@@ -140,9 +138,7 @@ class SmolagentsAdapter(BaseAdapter):
140
138
  ],
141
139
  )
142
140
  else:
143
- logger.warning(
144
- "No spans captured for Smolagents trace_id=%s", adapter_trace_id
145
- )
141
+ logger.warning("No spans captured for Smolagents trace_id=%s", trace_id)
146
142
 
147
143
  return AgentInvocationResponse(
148
144
  agent_trace=agent_trace,
@@ -48,9 +48,9 @@ class LocalAgentServer:
48
48
  Usage:
49
49
  ```python
50
50
  from quraite.serve.local_agent_server import LocalAgentServer
51
- from quraite.adapters import LangGraphAdapter
51
+ from quraite.adapters import LangChainAdapter
52
52
 
53
- sdk = LocalAgentServer(wrapped_agent=LangGraphAdapter(agent_graph=agent_graph))
53
+ sdk = LocalAgentServer(wrapped_agent=LangChainAdapter(agent_graph=agent_graph))
54
54
  sdk.start(host="0.0.0.0", port=8000, reload=False)
55
55
  ```
56
56
  """
@@ -227,15 +227,16 @@ class LocalAgentServer:
227
227
 
228
228
  logger.info("Local Agent Server started successfully")
229
229
  if self.public_url:
230
- logger.info("Agent publicly available at %s", self.public_url)
230
+ path = "/v1/agents/completions"
231
+ logger.info(f"Agent publicly available at {self.public_url}{path}")
232
+
231
233
  if not self.agent_id or not self._quraite_endpoint:
232
234
  logger.info(
233
- "Add this URL to your agent in the Quraite platform: %s",
234
- self.agent_url,
235
+ f"Add this URL to your agent in the Quraite platform: {self.public_url}{path}"
235
236
  )
236
237
  else:
237
238
  logger.info(
238
- "Agent running locally. Use a tunnel option to make it publicly available."
239
+ f"Agent running locally and available at {self.agent_url}{path}. Use a tunnel option to make it publicly available."
239
240
  )
240
241
  yield
241
242
 
@@ -12,6 +12,7 @@ __all__ = [
12
12
  "CostInfo",
13
13
  "TokenInfo",
14
14
  "Framework",
15
+ "Tool",
15
16
  "ToolCallInfo",
16
17
  "get_tool_extractor",
17
18
  "QuraiteInMemorySpanExporter",
@@ -1,15 +1,14 @@
1
1
  from enum import Enum
2
2
 
3
- QURAITE_ADAPTER_TRACE_PREFIX = "quraite-adapter"
4
-
5
3
  QURAITE_TRACER_NAME = "quraite.instrumentation"
6
4
 
7
5
 
8
6
  class Framework(str, Enum):
9
7
  """Supported agent frameworks."""
10
8
 
11
- PYDANTIC = "pydantic"
12
- LANGGRAPH = "langgraph"
9
+ DEFAULT = "default"
10
+ PYDANTIC_AI = "pydantic_ai"
11
+ LANGCHAIN = "langchain"
13
12
  GOOGLE_ADK = "google_adk"
14
13
  OPENAI_AGENTS = "openai_agents"
15
14
  AGNO = "agno"
@@ -12,20 +12,9 @@ class QuraiteInMemorySpanExporter(SpanExporter):
12
12
  def __init__(self) -> None:
13
13
  # self.spans: typing.List[ReadableSpan] = []
14
14
  self.traces: typing.Dict[int, typing.List[ReadableSpan]] = defaultdict(list)
15
- self.testcase_to_trace: typing.Dict[str, int] = {}
16
-
17
15
  self._stopped = False
18
16
  self._lock = threading.Lock()
19
17
 
20
- def handle_testcase_trace(self, span: ReadableSpan) -> None:
21
- """Handle a testcase trace."""
22
- # print(f"🟢 testcase trace received: {span.name}")
23
- # print(f"🟢 Span: {span.context}")
24
- formatted_trace_id = format(span.context.trace_id, "032x")[:8]
25
- # print(f"🟢 testcase formatted trace id: {formatted_trace_id}")
26
- self.traces[formatted_trace_id] = []
27
- self.testcase_to_trace[span.name] = formatted_trace_id
28
-
29
18
  def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult:
30
19
  """Stores a list of spans in memory."""
31
20
  if self._stopped:
@@ -33,13 +22,16 @@ class QuraiteInMemorySpanExporter(SpanExporter):
33
22
 
34
23
  with self._lock:
35
24
  for span in spans:
36
- formatted_trace_id = format(span.context.trace_id, "032x")[:8]
37
25
  # print(f"🟢 span formatted context trace id: {formatted_trace_id}")
38
- self.traces[formatted_trace_id].append(span)
26
+ self.traces[span.context.trace_id].append(span)
39
27
  # self.spans.append(span)
40
28
 
41
29
  return SpanExportResult.SUCCESS
42
30
 
31
+ def get_spans_by_trace_id(self, trace_id: int) -> typing.List[ReadableSpan]:
32
+ """Get all spans for a specific trace ID"""
33
+ return self.traces.get(trace_id, [])
34
+
43
35
  def shutdown(self) -> None:
44
36
  """Shut downs the exporter.
45
37
 
@@ -55,11 +47,6 @@ class QuraiteInMemorySpanExporter(SpanExporter):
55
47
  """Get all spans grouped by trace ID"""
56
48
  return dict(self.traces)
57
49
 
58
- def get_trace_by_testcase(self, testcase_name: str) -> typing.List[ReadableSpan]:
59
- """Get all spans for a specific testcase"""
60
- with self._lock:
61
- return self.traces.get(self.testcase_to_trace.get(testcase_name, None), [])
62
-
63
50
  def get_trace(self, trace_id: int) -> typing.List[ReadableSpan]:
64
51
  """Get all spans for a specific trace"""
65
52
  return self.traces.get(trace_id, [])
@@ -75,7 +62,6 @@ class QuraiteInMemorySpanExporter(SpanExporter):
75
62
  for trace_id, spans in self.traces.items():
76
63
  print(f"Trace ID: {trace_id}...")
77
64
  print(f"Spans in trace: {len(spans)}")
78
- print(f"Total Testcases: {self.testcase_to_trace}")
79
65
  print(f"TraceIDs: {self.traces.keys()}")
80
66
  print(f"{'='*60}\n")
81
67
 
@@ -10,7 +10,6 @@ from opentelemetry.context import (
10
10
  from opentelemetry.sdk.trace.export import ReadableSpan, SpanExporter, SpanProcessor
11
11
  from opentelemetry.trace import Span, logger
12
12
 
13
- from quraite.tracing.constants import QURAITE_ADAPTER_TRACE_PREFIX
14
13
  from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
15
14
 
16
15
 
@@ -27,8 +26,7 @@ class QuraiteSimpleSpanProcessor(SpanProcessor):
27
26
  def on_start(
28
27
  self, span: Span, parent_context: typing.Optional[Context] = None
29
28
  ) -> None:
30
- if QURAITE_ADAPTER_TRACE_PREFIX in span.name:
31
- self.span_exporter.handle_testcase_trace(span)
29
+ pass
32
30
 
33
31
  def on_end(self, span: ReadableSpan) -> None:
34
32
  if not span.context.trace_flags.sampled:
@@ -2,12 +2,14 @@
2
2
  Framework-specific tool extractors for converting span attributes to standardized tool call information.
3
3
 
4
4
  These extractors handle the varying attribute structures across different agent frameworks
5
- (pydantic, langgraph, adk, openai_agents, agno, smolagents, etc.)
5
+ (pydantic, langchain, adk, openai_agents, agno, smolagents, etc.)
6
6
  """
7
7
 
8
8
  import json
9
9
  from typing import Any, Protocol
10
10
 
11
+ from openinference.semconv.trace import SpanAttributes
12
+
11
13
  from quraite.tracing.constants import Framework
12
14
 
13
15
 
@@ -75,9 +77,9 @@ def extract_tool_pydantic(span: dict[str, Any]) -> ToolCallInfo | None:
75
77
  )
76
78
 
77
79
 
78
- def extract_tool_langgraph(span: dict[str, Any]) -> ToolCallInfo | None:
80
+ def extract_tool_langchain(span: dict[str, Any]) -> ToolCallInfo | None:
79
81
  """
80
- Extract tool info from LangGraph tool spans.
82
+ Extract tool info from LangChain tool spans.
81
83
 
82
84
  Attributes:
83
85
  - tool.name: "add"
@@ -94,7 +96,7 @@ def extract_tool_langgraph(span: dict[str, Any]) -> ToolCallInfo | None:
94
96
  arguments = attrs.get("input.value", "{}")
95
97
  output_value = attrs.get("output.value", "")
96
98
 
97
- # Also check for response attribute (some LangGraph spans store response here)
99
+ # Also check for response attribute (some LangChain spans store response here)
98
100
  response_value = attrs.get("response", output_value)
99
101
 
100
102
  # Try to parse output to extract content
@@ -115,7 +117,7 @@ def extract_tool_langgraph(span: dict[str, Any]) -> ToolCallInfo | None:
115
117
  # First check for direct content field
116
118
  if "content" in parsed:
117
119
  response = parsed.get("content", response_value)
118
- # Check for update.messages structure (LangGraph graph updates)
120
+ # Check for update.messages structure (LangChain graph updates)
119
121
  # this comes when you use supervisor agent with multiple agents
120
122
  elif "update" in parsed:
121
123
  update = parsed.get("update", {})
@@ -137,7 +139,7 @@ def extract_tool_langgraph(span: dict[str, Any]) -> ToolCallInfo | None:
137
139
 
138
140
  return ToolCallInfo(
139
141
  tool_name=tool_name,
140
- tool_call_id=None, # LangGraph doesn't always have call IDs in tool spans
142
+ tool_call_id=None, # LangChain doesn't always have call IDs in tool spans
141
143
  arguments=arguments,
142
144
  response=response,
143
145
  )
@@ -270,14 +272,33 @@ def extract_tool_smolagents(span: dict[str, Any]) -> ToolCallInfo | None:
270
272
  )
271
273
 
272
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
+
273
293
  # Registry of framework extractors
274
294
  TOOL_EXTRACTORS: dict[Framework, ToolExtractor] = {
275
- Framework.PYDANTIC: extract_tool_pydantic,
276
- Framework.LANGGRAPH: extract_tool_langgraph,
295
+ Framework.PYDANTIC_AI: extract_tool_pydantic,
296
+ Framework.LANGCHAIN: extract_tool_langchain,
277
297
  Framework.GOOGLE_ADK: extract_tool_adk,
278
298
  Framework.OPENAI_AGENTS: extract_tool_openai_agents,
279
299
  Framework.AGNO: extract_tool_agno,
280
300
  Framework.SMOLAGENTS: extract_tool_smolagents,
301
+ Framework.DEFAULT: extract_default,
281
302
  }
282
303
 
283
304
 
@@ -285,6 +306,4 @@ def get_tool_extractor(framework: Framework | str) -> ToolExtractor:
285
306
  """Get the appropriate tool extractor for the given framework."""
286
307
  if isinstance(framework, str):
287
308
  framework = Framework(framework.lower())
288
- return TOOL_EXTRACTORS.get(
289
- framework, extract_tool_langgraph
290
- ) # Default to langgraph
309
+ return TOOL_EXTRACTORS.get(framework, extract_default)
quraite/tracing/trace.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  from functools import cached_property
6
- from typing import Any, List
6
+ from typing import Any, List, Optional
7
7
 
8
8
  from openinference.semconv.trace import OpenInferenceSpanKindValues, SpanAttributes
9
9
  from opentelemetry.sdk.trace import ReadableSpan
@@ -185,7 +185,7 @@ class AgentSpan(BaseModel):
185
185
  return cleaned_messages
186
186
 
187
187
  def to_tool_message(
188
- self, framework: Framework = Framework.LANGGRAPH
188
+ self, framework: Framework = Framework.LANGCHAIN
189
189
  ) -> dict[str, Any] | None:
190
190
  """
191
191
  Convert tool execution span to a tool message.
@@ -405,8 +405,8 @@ class AgentTrace(BaseModel):
405
405
 
406
406
  def to_agent_trajectory(
407
407
  self,
408
- framework: Framework = Framework.LANGGRAPH,
409
- only_leaf_llms: bool = True,
408
+ framework: Optional[Framework] = Framework.DEFAULT,
409
+ only_leaf_llms: Optional[bool] = True,
410
410
  ) -> List[AssistantMessage | ToolMessage]:
411
411
  """
412
412
  Convert trace spans to agent trajectory.
@@ -422,9 +422,9 @@ class AgentTrace(BaseModel):
422
422
  # - Some frameworks (pydantic_ai, agno) execute tools DURING the LLM span
423
423
  # (tools are nested inside LLM), so we sort by end_time to get correct order
424
424
  # This means the parent span does not end before the nested span ends.
425
- # - Other frameworks (langgraph, openai_agents, etc.) execute tools AFTER the LLM span
425
+ # - Other frameworks (langchain, openai_agents, etc.) execute tools AFTER the LLM span
426
426
  # ends, so start_time sort works fine
427
- nested_tool_frameworks = (Framework.PYDANTIC, Framework.AGNO)
427
+ nested_tool_frameworks = (Framework.PYDANTIC_AI, Framework.AGNO)
428
428
 
429
429
  if framework in nested_tool_frameworks:
430
430
  # Sort by end_time: LLM output is ready at end_time, then tool results follow
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quraite
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: This project provides adaptors and methods to integrate with the Quraite platform
5
5
  Author: Shiv Mohith
6
6
  Author-email: Shiv Mohith <shivmohith8@gmail.com>
@@ -16,8 +16,8 @@ Requires-Dist: uvicorn>=0.38.0
16
16
  Requires-Dist: agno>=2.3.4 ; extra == 'agno'
17
17
  Requires-Dist: boto3>=1.40.70 ; extra == 'bedrock'
18
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'
19
+ Requires-Dist: langchain>=1.0.5 ; extra == 'langchain'
20
+ Requires-Dist: langgraph>=1.0.3 ; extra == 'langchain'
21
21
  Requires-Dist: openai-agents>=0.5.0 ; extra == 'openai-agents'
22
22
  Requires-Dist: pydantic-ai>=1.25.0 ; extra == 'pydantic-ai'
23
23
  Requires-Dist: pyngrok>=7.5.0 ; extra == 'pyngrok'
@@ -26,7 +26,7 @@ Requires-Python: >=3.10
26
26
  Provides-Extra: agno
27
27
  Provides-Extra: bedrock
28
28
  Provides-Extra: google-adk
29
- Provides-Extra: langgraph
29
+ Provides-Extra: langchain
30
30
  Provides-Extra: openai-agents
31
31
  Provides-Extra: pydantic-ai
32
32
  Provides-Extra: pyngrok
@@ -37,16 +37,15 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
39
39
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
40
+ [![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-Compatible-blue)](https://opentelemetry.io/)
40
41
 
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
+ The **Quraite Python SDK** provides adapters and methods to integrate AI agent with the [Quraite platform](https://quraite.ai) for evaluation. It offers a unified interface for different agent frameworks, automatic tracing at every turn for agent trajectory evaluation, and easy local server setup with tunneling capabilities.
42
43
 
43
44
  ## Features
44
45
 
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
46
+ - 🔌 **Framework Adapters**: Support for multiple AI agent frameworks (LangChain, Pydantic AI, Agno, Google ADK, OpenAI Agents, Smolagents, AWS Bedrock, Flowise, Langflow, N8n, and more)
47
+ - 📊 **Automatic Tracing**: Built-in OpenInference-based (OpenTelemetry-based tracing support coming soon) tracing for agent trajectory evaluation. Track token usage, costs, latency, and model information for each agent invocation
48
+ - 🚀 **Local Server**: Easy-to-use local server with optional tunneling (Cloudflare/ngrok) for public access and integration with Quraite platform
50
49
 
51
50
  ## Installation
52
51
 
@@ -61,8 +60,8 @@ pip install quraite
61
60
  Install with optional dependencies for specific frameworks:
62
61
 
63
62
  ```bash
64
- # LangGraph
65
- pip install 'quraite[langgraph]'
63
+ # LangChain
64
+ pip install 'quraite[langchain]'
66
65
 
67
66
  # Pydantic AI
68
67
  pip install 'quraite[pydantic-ai]'
@@ -83,42 +82,45 @@ pip install 'quraite[smolagents]'
83
82
  pip install 'quraite[bedrock]'
84
83
 
85
84
  # Multiple frameworks
86
- pip install 'quraite[langgraph,pydantic-ai,agno]'
85
+ pip install 'quraite[langchain,pydantic-ai,agno]'
87
86
  ```
88
87
 
89
88
  ## Quick Start
90
89
 
91
- ### Example: LangGraph Agent with Local Server
90
+ ### Example: LangChain Agent with Local Server
92
91
 
93
- Pass your compiled LangGraph agent to the adapter and expose it as an HTTP API:
92
+ Pass your compiled LangChain agent to the adapter and expose it as an HTTP API:
94
93
 
95
94
  ```python
95
+ import asyncio
96
96
  import uvicorn
97
97
  from dotenv import load_dotenv
98
98
  from openinference.instrumentation import TracerProvider
99
99
  from openinference.instrumentation.langchain import LangChainInstrumentor
100
100
 
101
- from quraite.adapters import LanggraphAdapter
101
+ from quraite.adapters import LangChainAdapter
102
102
  from quraite.serve.local_agent import LocalAgentServer
103
103
  from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
104
104
  from quraite.tracing.span_processor import QuraiteSimpleSpanProcessor
105
105
 
106
106
  load_dotenv()
107
107
 
108
- # Set up tracing (optional)
108
+ # Set up tracing
109
+ # Use Quraite's in-memory span exporter to capture the agent trajectory
110
+ # and use it for evaluation.
109
111
  tracer_provider = TracerProvider()
110
112
  quraite_span_exporter = QuraiteInMemorySpanExporter()
111
113
  quraite_span_processor = QuraiteSimpleSpanProcessor(quraite_span_exporter)
112
114
  tracer_provider.add_span_processor(quraite_span_processor)
113
115
  LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
114
116
 
115
- # Your compiled LangGraph agent (created elsewhere)
117
+ # Your compiled LangChain agent (created elsewhere)
116
118
  # agent = create_agent(...)
117
119
 
118
120
  # 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
121
+ adapter = LangChainAdapter(
122
+ agent_graph=agent, # Pass your compiled LangChain agent here
123
+ tracer_provider=tracer_provider,
122
124
  )
123
125
 
124
126
  # Create and start server with Cloudflare tunnel
@@ -133,14 +135,18 @@ app = server.create_app(
133
135
  tunnel="cloudflare", # Options: "cloudflare", "ngrok", or "none"
134
136
  )
135
137
 
136
- if __name__ == "__main__":
137
- uvicorn.run("local_server:app", host="0.0.0.0", port=8080)
138
+ # Option 1: Use the start method to start the server
139
+ asyncio.run(server.start(host="0.0.0.0", port=8080))
140
+
141
+ # Option 2: Use uvicorn to start the server for auto-reload
142
+ # if __name__ == "__main__":
143
+ # uvicorn.run("local_server:app", host="0.0.0.0", port=8080, reload=True)
138
144
  ```
139
145
 
140
146
  The server exposes:
141
147
 
142
148
  - `GET /` - Health check endpoint
143
- - `POST /v1/agents/completions` - Agent invocation endpoint
149
+ - `POST /v1/agents/completions` - Agent invocation endpoint. This is the endpoint that Quraite will use to invoke your agent.
144
150
 
145
151
  When using `tunnel="cloudflare"` or `tunnel="ngrok"`, your agent will be publicly accessible via the generated URL.
146
152
 
@@ -148,7 +154,7 @@ When using `tunnel="cloudflare"` or `tunnel="ngrok"`, your agent will be publicl
148
154
 
149
155
  | Framework | Adapter | Installation |
150
156
  | -------------------- | ------------------------ | -------------------------------------- |
151
- | **LangGraph** | `LanggraphAdapter` | `pip install 'quraite[langgraph]'` |
157
+ | **LangChain** | `LangChainAdapter` | `pip install 'quraite[langchain]'` |
152
158
  | **Pydantic AI** | `PydanticAIAdapter` | `pip install 'quraite[pydantic-ai]'` |
153
159
  | **Agno** | `AgnoAdapter` | `pip install 'quraite[agno]'` |
154
160
  | **Google ADK** | `GoogleADKAdapter` | `pip install 'quraite[google-adk]'` |
@@ -159,27 +165,28 @@ When using `tunnel="cloudflare"` or `tunnel="ngrok"`, your agent will be publicl
159
165
  | **Langflow** | `LangflowAdapter` | Included in base package |
160
166
  | **N8n** | `N8nAdapter` | Included in base package |
161
167
  | **HTTP** | `HttpAdapter` | Included in base package |
162
- | **LangGraph Server** | `LanggraphServerAdapter` | `pip install 'quraite[langgraph]'` |
168
+ | **LangChain Server** | `LangChainServerAdapter` | `pip install 'quraite[langchain]'` |
163
169
 
164
170
  ## Core Concepts
165
171
 
166
172
  ### Adapters
167
173
 
168
- Adapters provide a unified interface (`BaseAdapter`) for different agent frameworks. Each adapter:
174
+ Adapters provide a unified interface (`BaseAdapter`) for different agent frameworks. Each adapter converts framework-specific agent response formats to the Quraite agent message format.
175
+
176
+ If you are building your own agent framework, you can create a custom adapter by extending the `BaseAdapter` class and implementing the `ainvoke` method.
177
+
178
+ ### Tracing for Agent Trajectory Evaluation
169
179
 
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()`
180
+ **Capture agent trajectories without modifying your code.** Get comprehensive trace data including token usage, costs, and latency for every agent step.
174
181
 
175
- ### Tracing
182
+ Most agent frameworks return agent steps, but lack critical observability data. We solve this with **OpenInference instrumentation** (OpenTelemetry instrumentation support coming soon) that automatically captures:
176
183
 
177
- The SDK includes built-in OpenTelemetry-based tracing that captures:
184
+ - Complete agent trajectories
185
+ - Token usage and costs
186
+ - Step-by-step latency
187
+ - Full execution context
178
188
 
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
189
+ **Works with your existing setup.** We provide OpenInference-compatible span exporters and processors that integrate seamlessly with your current observability platform - no vendor lock-in required.
183
190
 
184
191
  To enable tracing:
185
192
 
@@ -189,11 +196,13 @@ from quraite.tracing.span_exporter import QuraiteInMemorySpanExporter
189
196
  from quraite.tracing.span_processor import QuraiteSimpleSpanProcessor
190
197
 
191
198
  tracer_provider = TracerProvider()
199
+
200
+ # Add Quraite span exporter and processor to the tracer provider
192
201
  quraite_span_exporter = QuraiteInMemorySpanExporter()
193
202
  quraite_span_processor = QuraiteSimpleSpanProcessor(quraite_span_exporter)
194
203
  tracer_provider.add_span_processor(quraite_span_processor)
195
204
 
196
- # Instrument your framework (example for LangChain)
205
+ # Instrument your framework with OpenInference
197
206
  from openinference.instrumentation.langchain import LangChainInstrumentor
198
207
  LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
199
208
  ```
@@ -251,18 +260,15 @@ response: AgentInvocationResponse = await adapter.ainvoke(
251
260
  # Access trajectory (list of messages)
252
261
  trajectory = response.agent_trajectory
253
262
 
254
- # Access trace (if tracing enabled)
263
+ # Access trace
255
264
  trace = response.agent_trace
256
-
257
- # Access final response text
258
- final_response = response.agent_final_response
259
265
  ```
260
266
 
261
267
  ## Examples
262
268
 
263
269
  The repository includes comprehensive examples for each supported framework:
264
270
 
265
- - [`langgraph_calculator_agent`](examples/langgraph_calculator_agent/) - LangGraph calculator agent
271
+ - [`langchain_calculator_agent`](examples/langchain_calculator_agent/) - LangChain calculator agent
266
272
  - [`pydantic_calculator_agent`](examples/pydantic_calculator_agent/) - Pydantic AI calculator agent
267
273
  - [`agno_calculator_agent`](examples/agno_calculator_agent/) - Agno calculator agent
268
274
  - [`google_adk_weather_agent`](examples/google_adk_weather_agent/) - Google ADK weather agent
@@ -302,6 +308,7 @@ class MyAdapter(BaseAdapter):
302
308
  Create a local HTTP server for your agent:
303
309
 
304
310
  ```python
311
+ import asyncio
305
312
  from quraite.serve.local_agent import LocalAgentServer
306
313
 
307
314
  server = LocalAgentServer(
@@ -314,6 +321,8 @@ app = server.create_app(
314
321
  host="0.0.0.0",
315
322
  tunnel="cloudflare", # or "ngrok" or "none"
316
323
  )
324
+
325
+ asyncio.run(server.start(host="0.0.0.0", port=8080))
317
326
  ```
318
327
 
319
328
  ## Development
@@ -1,25 +1,25 @@
1
1
  quraite/__init__.py,sha256=vxU3HsbCj6AT_rl5bZJA-e3R_bzqLhMGCe96_KM4Biw,48
2
- quraite/adapters/__init__.py,sha256=Uw2dV1_Acj_hT8N1GnQzVQEIMm_3BvZzLkWc5KfhbMw,4842
3
- quraite/adapters/agno_adapter.py,sha256=savrQqE0iuXyFprjlhFNZ-WpZpkfqJ9Y4OeaddCtQ_Y,5639
4
- quraite/adapters/base.py,sha256=sgzXuCRt81LyH7kjP3UsccbgDbgVQeHKOfqcXah1mHo,3917
2
+ quraite/adapters/__init__.py,sha256=-2FPY_cUHtIKVlB7O3SUkgrEp1iybJ1z3VM52SjYk3I,4842
3
+ quraite/adapters/agno_adapter.py,sha256=mZukC0ItxhhreHQB4V2xPDXJ7JUXpEG60rtj25_iRdg,5572
4
+ quraite/adapters/base.py,sha256=7wRwmcjLg73cec_VKk2jwwPLyDRiutgtCuREnpgyVYw,3928
5
5
  quraite/adapters/bedrock_agents_adapter.py,sha256=2G-zUXqhNSzpyxut2UQgqTfe01MWcdjt1ajZIb43CUM,14136
6
6
  quraite/adapters/flowise_adapter.py,sha256=yLJ0tjDWMtTzm0bRYbhjfRm6wd5A6DqbHhqemy_FW48,10349
7
- quraite/adapters/google_adk_adapter.py,sha256=Ci7L54gA6pEH3-qcZ0PW1qODV6flJsVVbL9sN78FT_Y,7390
8
- quraite/adapters/http_adapter.py,sha256=y5ft65fF6SfvJXkhDkoglvq6dv6_VlCWDTZSCLEgssE,7883
7
+ quraite/adapters/google_adk_adapter.py,sha256=ObVojr9sChciayYaZzrCwXUk4kjqdEMMRm4ObYqPmcE,7433
8
+ quraite/adapters/http_adapter.py,sha256=HY9qRj_enxED73TaEIPN06l9ooqkLjBiPHssBdMPLJM,7990
9
+ quraite/adapters/langchain_adapter.py,sha256=PnJUMUVMJptoOE13gIMYkbq3h3ZF1ihEEYroWZQzuSs,11581
10
+ quraite/adapters/langchain_server_adapter.py,sha256=t5wUZrpIRtPN-CbRK3-I0yFfdTpvsbqFzZgrrvkleLQ,9694
9
11
  quraite/adapters/langflow_adapter.py,sha256=zGbDSd7Q_AZZ0ICByedsBIl5yNJ0hN6tQZgpUyN41-E,6946
10
- quraite/adapters/langgraph_adapter.py,sha256=WqmbP6Zw1lwfSCBf_O7oKNoGEb_1-zw_PMS1v71i3-w,11603
11
- quraite/adapters/langgraph_server_adapter.py,sha256=OhMx370Uzrvbp4yMOtgKkjQFI5u1xboKq0ybQRsBD90,9694
12
12
  quraite/adapters/n8n_adapter.py,sha256=CCIOcZNxQw-69xdf9BVX9Jyt8i666AlsE2DJHsY8lUc,7967
13
- quraite/adapters/openai_agents_adapter.py,sha256=iCFOWrZ-HnLoWOB-xsyJZf37Nh5QW5j8sHNSkjSWdGc,9665
14
- quraite/adapters/pydantic_ai_adapter.py,sha256=gps_90Zp8NcAiDX26-py4MKN2B5_jPEvgroRi8kjfQU,10811
15
- quraite/adapters/smolagents_adapter.py,sha256=QhEyMDlFkkXPSc5PiQTxMA9i2fRitzNcMdEsZwe9QVY,5448
13
+ quraite/adapters/openai_agents_adapter.py,sha256=J404wIy9OyUOB0wTtEy6RIiKijMfQk1oLxu1vC7Je5U,9595
14
+ quraite/adapters/pydantic_ai_adapter.py,sha256=BeP0En6gHxszrwwnpM0NJJ7dtkJTB3YNYG4EktJAL0w,10698
15
+ quraite/adapters/smolagents_adapter.py,sha256=rFnHkF4oHOWQRq2nfIluwWEQwxFk2xbFfJupiu90L-k,5345
16
16
  quraite/logger.py,sha256=Py5GRQfD_s5FVb2Ziz5JxJk9AT4ve6w_iePbOSQkwH4,1601
17
17
  quraite/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
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=8-a_4AeHeK3nTsgoDJVcTx7s4YvPoUhd6Zu3KVWEVAQ,12845
22
+ quraite/serve/local_agent.py,sha256=G398f-kqzPjFEaaFuh4i-Y0qtrimP0SLSO7kyAPxcqU,12921
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
@@ -34,16 +34,16 @@ quraite/traces/traces_pydantic_openinference_multi_agent_1.json,sha256=FjiPOvbhz
34
34
  quraite/traces/traces_pydantic_openinference_multi_agent_2.json,sha256=bV3z56JmzoLmxdDvL4RE9opgq3kFnMxNJFD5D4CbDtw,24751
35
35
  quraite/traces/traces_smol_agents_openinference.json,sha256=JVgRniA-VCuQ0oKWSqW0d09K4RcRFoOq5pcbejBigAc,83161
36
36
  quraite/traces/traces_smol_agents_tool_calling_openinference.json,sha256=QnKnCBukoNyR8ufTPLbjpWBFSguBRSUyd0-zLMNL2sw,217626
37
- quraite/tracing/__init__.py,sha256=ZAI6uLooKU8n6Np3TIAS8JN7-6bAdtajeT1PdFU-4_8,751
38
- quraite/tracing/constants.py,sha256=KhBIHkHRXAGW6xqa9vFa1LnA94C-EUTrWh5zNsx_z4Y,358
39
- quraite/tracing/span_exporter.py,sha256=0S3qyPgtPfZtb-IhEoQKm0ciFZoHvlaFUcqbNtDh7kM,4676
40
- quraite/tracing/span_processor.py,sha256=mp5AR5RbRz1ddgJEK07CbXRL8NfaFj2DXP1bs9xcf4w,1603
41
- quraite/tracing/tool_extractors.py,sha256=BXTr0lT570jSlQtwLvFGarEvBSkvAs07YSo6Ah9Qkkw,9232
42
- quraite/tracing/trace.py,sha256=bLIXyk8XXRC6LcPImDnwowW20eg2WI6qUW6-SFrTLpQ,21646
37
+ quraite/tracing/__init__.py,sha256=uDML55hjgQ8W2thaAcZFFooy4aEIExxKzeBFXJGVPm4,763
38
+ quraite/tracing/constants.py,sha256=W2z9RsDMvXfCpyhhlm_5E0EVjw-a9GGNq6wHliM3A_g,338
39
+ quraite/tracing/span_exporter.py,sha256=Y5bJDwNeV62s3UIAykOSvTC3eOSuwYH9j0L4sxhZK-I,3940
40
+ quraite/tracing/span_processor.py,sha256=GKxZGL5D71YVGukns2-tpnng8n1Ds8M1w3D7FIBETZg,1436
41
+ quraite/tracing/tool_extractors.py,sha256=6YuCcb_aL_lyRJAp_A5-kZVK29ARU73mqjmiJnjtbGU,9800
42
+ quraite/tracing/trace.py,sha256=wumQwANCw9KY_pMDjS-10EO8lyO8qHxKZ02Dnq74WgI,21677
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.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,,
47
+ quraite-0.1.3.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
48
+ quraite-0.1.3.dist-info/METADATA,sha256=82tXqkmPtK4XJcZFNI_fZuBIZjLFWJHoCX2kFJB02TQ,12063
49
+ quraite-0.1.3.dist-info/RECORD,,