qveris 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qveris/__init__.py +34 -0
- qveris/agent/__init__.py +4 -0
- qveris/agent/core.py +415 -0
- qveris/agent/memory.py +56 -0
- qveris/client/__init__.py +21 -0
- qveris/client/api.py +425 -0
- qveris/client/tools.py +115 -0
- qveris/config.py +74 -0
- qveris/generated/__init__.py +17 -0
- qveris/generated/openapi_models.py +574 -0
- qveris/llm/__init__.py +4 -0
- qveris/llm/base.py +108 -0
- qveris/llm/openai/__init__.py +4 -0
- qveris/llm/openai/config.py +14 -0
- qveris/llm/openai/provider.py +185 -0
- qveris/types.py +256 -0
- qveris-0.2.0.dist-info/METADATA +18 -0
- qveris-0.2.0.dist-info/RECORD +20 -0
- qveris-0.2.0.dist-info/WHEEL +4 -0
- qveris-0.2.0.dist-info/licenses/LICENSE +21 -0
qveris/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .agent.core import Agent
|
|
2
|
+
from .client.api import QverisClient
|
|
3
|
+
from .config import QverisConfig, AgentConfig
|
|
4
|
+
from .types import (
|
|
5
|
+
CompactBillingStatement,
|
|
6
|
+
CreditsLedgerItem,
|
|
7
|
+
CreditsLedgerResponse,
|
|
8
|
+
Message,
|
|
9
|
+
SearchResponse,
|
|
10
|
+
StreamEvent,
|
|
11
|
+
ToolExecutionResponse,
|
|
12
|
+
ToolInfo,
|
|
13
|
+
ToolParameter,
|
|
14
|
+
UsageEventItem,
|
|
15
|
+
UsageHistoryResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Agent",
|
|
20
|
+
"QverisClient",
|
|
21
|
+
"QverisConfig",
|
|
22
|
+
"AgentConfig",
|
|
23
|
+
"Message",
|
|
24
|
+
"StreamEvent",
|
|
25
|
+
"ToolInfo",
|
|
26
|
+
"ToolParameter",
|
|
27
|
+
"SearchResponse",
|
|
28
|
+
"ToolExecutionResponse",
|
|
29
|
+
"CompactBillingStatement",
|
|
30
|
+
"UsageEventItem",
|
|
31
|
+
"UsageHistoryResponse",
|
|
32
|
+
"CreditsLedgerItem",
|
|
33
|
+
"CreditsLedgerResponse",
|
|
34
|
+
]
|
qveris/agent/__init__.py
ADDED
qveris/agent/core.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qveris agent runtime.
|
|
3
|
+
|
|
4
|
+
This module defines `Agent`, the high-level orchestration layer that connects:
|
|
5
|
+
|
|
6
|
+
- an LLM provider (`LLMProvider`) capable of emitting tool calls,
|
|
7
|
+
- Qveris built-in tools (`discover`, `inspect`, `call`) exposed to the LLM,
|
|
8
|
+
- optional user-provided tools and a handler for those tools,
|
|
9
|
+
- and an execution loop that feeds tool results back to the LLM until completion.
|
|
10
|
+
|
|
11
|
+
## Event model
|
|
12
|
+
|
|
13
|
+
`Agent.run(...)` yields `StreamEvent` objects. Depending on the chosen provider and whether
|
|
14
|
+
streaming is enabled, you may see:
|
|
15
|
+
|
|
16
|
+
- `content`: assistant output (delta chunks in streaming mode, whole message in non-streaming)
|
|
17
|
+
- `reasoning`: optional reasoning tokens from some providers
|
|
18
|
+
- `reasoning_details`: optional structured reasoning details (e.g. Gemini thought signatures via OpenRouter)
|
|
19
|
+
- `tool_call`: a tool call the model wants to invoke (OpenAI-compatible tool-call dict)
|
|
20
|
+
- `tool_result`: the result of executing a tool call (Qveris built-ins or your extra tools)
|
|
21
|
+
- `metrics`: token usage / timing metrics if the provider reports them
|
|
22
|
+
- `error`: fatal error that stops the run
|
|
23
|
+
|
|
24
|
+
## Tool call lifecycle
|
|
25
|
+
|
|
26
|
+
When the LLM requests one or more tool calls:
|
|
27
|
+
|
|
28
|
+
1. the assistant message (with tool calls) is appended to the conversation,
|
|
29
|
+
2. each tool is executed in sequence,
|
|
30
|
+
3. the tool results are appended as `role="tool"` messages,
|
|
31
|
+
4. the loop continues with the updated conversation.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import inspect
|
|
35
|
+
import json
|
|
36
|
+
import uuid
|
|
37
|
+
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional
|
|
38
|
+
|
|
39
|
+
import httpx
|
|
40
|
+
from openai import APIConnectionError, APIStatusError, APITimeoutError, AuthenticationError, RateLimitError
|
|
41
|
+
from openai.types.chat import ChatCompletionToolParam
|
|
42
|
+
|
|
43
|
+
from ..client.api import QverisClient
|
|
44
|
+
from ..client.tools import CALL_TOOL_DEF, DEFAULT_SYSTEM_PROMPT, DISCOVER_TOOL_DEF, INSPECT_TOOL_DEF
|
|
45
|
+
from ..config import AgentConfig, QverisConfig
|
|
46
|
+
from ..llm.base import LLMProvider
|
|
47
|
+
from ..llm.openai import OpenAIProvider
|
|
48
|
+
from ..types import ChatResponse, Message, StreamEvent
|
|
49
|
+
from .memory import prune_tool_history
|
|
50
|
+
|
|
51
|
+
# Type alias for extra tool handler callback.
|
|
52
|
+
# Called only when the tool call is NOT a built-in Qveris tool.
|
|
53
|
+
ExtraToolHandler = Callable[[str, Dict[str, Any]], Awaitable[Any]]
|
|
54
|
+
|
|
55
|
+
class Agent:
|
|
56
|
+
"""
|
|
57
|
+
Qveris agent orchestrator.
|
|
58
|
+
|
|
59
|
+
The agent runs an LLM/tool loop that can:
|
|
60
|
+
|
|
61
|
+
- discover capabilities via Qveris (`discover`),
|
|
62
|
+
- inspect candidate capabilities (`inspect`),
|
|
63
|
+
- call a selected capability (`call`),
|
|
64
|
+
- optionally execute additional user-provided tools (`extra_tools` + `extra_tool_handler`).
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
config:
|
|
68
|
+
Qveris API / agent runtime configuration (API key, base URL, max iterations, etc.).
|
|
69
|
+
agent_config:
|
|
70
|
+
LLM configuration (model name, temperature, additional system prompt, ...).
|
|
71
|
+
llm_provider:
|
|
72
|
+
Provider implementation that follows `LLMProvider`. If omitted, uses the built-in
|
|
73
|
+
OpenAI-compatible provider (`OpenAIProvider`).
|
|
74
|
+
extra_tools:
|
|
75
|
+
Optional additional tool schemas (OpenAI `ChatCompletionToolParam`) exposed to the LLM.
|
|
76
|
+
These are **not** executed by Qveris unless you also provide `extra_tool_handler`.
|
|
77
|
+
extra_tool_handler:
|
|
78
|
+
Async callback invoked for non-Qveris tool calls. Signature:
|
|
79
|
+
`async def handler(func_name: str, func_args: dict) -> Any`.
|
|
80
|
+
debug_callback:
|
|
81
|
+
Optional callback used by `QverisClient` to emit debug messages (request/response logs,
|
|
82
|
+
with authorization redacted).
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
- A session id is created at construction time; call `new_session()` to reset it.
|
|
86
|
+
- This class is safe to reuse across multiple conversations; pass your own `messages` list.
|
|
87
|
+
"""
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
config: Optional[QverisConfig] = None,
|
|
91
|
+
agent_config: Optional[AgentConfig] = None,
|
|
92
|
+
llm_provider: Optional[LLMProvider] = None,
|
|
93
|
+
extra_tools: Optional[List[ChatCompletionToolParam]] = None,
|
|
94
|
+
extra_tool_handler: Optional[ExtraToolHandler] = None,
|
|
95
|
+
debug_callback: Optional[Callable[[str], None]] = None
|
|
96
|
+
):
|
|
97
|
+
self.config = config or QverisConfig()
|
|
98
|
+
self.agent_config = agent_config or AgentConfig()
|
|
99
|
+
|
|
100
|
+
# Setup API Client with debug callback
|
|
101
|
+
self.client = QverisClient(self.config, debug_callback=debug_callback)
|
|
102
|
+
|
|
103
|
+
# Setup LLM Provider
|
|
104
|
+
if llm_provider:
|
|
105
|
+
self.llm = llm_provider
|
|
106
|
+
else:
|
|
107
|
+
# Fallback to internal OpenAI provider
|
|
108
|
+
self.llm = OpenAIProvider()
|
|
109
|
+
|
|
110
|
+
self.tools: List[ChatCompletionToolParam] = [DISCOVER_TOOL_DEF, INSPECT_TOOL_DEF, CALL_TOOL_DEF]
|
|
111
|
+
if extra_tools:
|
|
112
|
+
self.tools.extend(extra_tools)
|
|
113
|
+
|
|
114
|
+
# Handler for extra tools not built into Qveris
|
|
115
|
+
self.extra_tool_handler = extra_tool_handler
|
|
116
|
+
|
|
117
|
+
# Setup new session
|
|
118
|
+
self.new_session()
|
|
119
|
+
|
|
120
|
+
self.last_messages: List[Message] = []
|
|
121
|
+
|
|
122
|
+
async def close(self) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Close network resources owned by the agent.
|
|
125
|
+
|
|
126
|
+
Call this when you are done with a long-lived `Agent`, or use the agent as an async
|
|
127
|
+
context manager so cleanup happens automatically.
|
|
128
|
+
"""
|
|
129
|
+
await self.client.close()
|
|
130
|
+
|
|
131
|
+
close_llm = getattr(self.llm, "close", None)
|
|
132
|
+
if callable(close_llm):
|
|
133
|
+
result = close_llm()
|
|
134
|
+
if inspect.isawaitable(result):
|
|
135
|
+
await result
|
|
136
|
+
|
|
137
|
+
async def __aenter__(self) -> "Agent":
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
141
|
+
await self.close()
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _llm_error_event(error: Exception) -> StreamEvent:
|
|
145
|
+
"""Convert provider/client exceptions into user-facing agent error events."""
|
|
146
|
+
if isinstance(error, httpx.TimeoutException):
|
|
147
|
+
return StreamEvent(type="error", error=f"LLM request timed out: {error}")
|
|
148
|
+
if isinstance(error, httpx.ConnectError):
|
|
149
|
+
return StreamEvent(type="error", error=f"Failed to connect to LLM service: {error}")
|
|
150
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
151
|
+
return StreamEvent(
|
|
152
|
+
type="error",
|
|
153
|
+
error=f"LLM HTTP error {error.response.status_code}: {error.response.text[:200]}"
|
|
154
|
+
)
|
|
155
|
+
if isinstance(error, AuthenticationError):
|
|
156
|
+
return StreamEvent(type="error", error=f"LLM authentication failed: {error}")
|
|
157
|
+
if isinstance(error, RateLimitError):
|
|
158
|
+
return StreamEvent(type="error", error=f"LLM rate limit exceeded: {error}")
|
|
159
|
+
if isinstance(error, (APIConnectionError, APITimeoutError)):
|
|
160
|
+
return StreamEvent(type="error", error=f"LLM connection error: {error}")
|
|
161
|
+
if isinstance(error, APIStatusError):
|
|
162
|
+
return StreamEvent(type="error", error=f"LLM API error {error.status_code}: {error}")
|
|
163
|
+
return StreamEvent(type="error", error=f"LLM error: {error}")
|
|
164
|
+
|
|
165
|
+
def _record_last_messages(self, current_messages: List[Message], inserted_system_prompt: bool) -> None:
|
|
166
|
+
messages_for_caller = current_messages[1:] if inserted_system_prompt else current_messages
|
|
167
|
+
self.last_messages = [message.model_copy(deep=True) for message in messages_for_caller]
|
|
168
|
+
|
|
169
|
+
def get_last_messages(self) -> List[Message]:
|
|
170
|
+
"""
|
|
171
|
+
Return the latest conversation history produced by `run(...)`.
|
|
172
|
+
|
|
173
|
+
The returned history includes intermediate assistant tool calls and tool results, plus the
|
|
174
|
+
final assistant content when one was produced. If `run(...)` injected the default system
|
|
175
|
+
prompt, that internal system message is omitted so callers can reuse the list directly.
|
|
176
|
+
"""
|
|
177
|
+
return [message.model_copy(deep=True) for message in self.last_messages]
|
|
178
|
+
|
|
179
|
+
async def run(
|
|
180
|
+
self,
|
|
181
|
+
messages: List[Message],
|
|
182
|
+
stream: bool = True
|
|
183
|
+
) -> AsyncGenerator[StreamEvent, None]:
|
|
184
|
+
"""
|
|
185
|
+
Run the agent loop and yield events as they occur.
|
|
186
|
+
|
|
187
|
+
This is the primary integration API. In streaming mode (`stream=True`), the underlying
|
|
188
|
+
provider is expected to yield delta `content` chunks; in non-streaming mode, this method
|
|
189
|
+
yields a single `content` event for the assistant message.
|
|
190
|
+
|
|
191
|
+
Tool calls are always surfaced as `tool_call` events, and tool executions as `tool_result`.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
messages: Conversation history (typically starts with `role="user"`).
|
|
195
|
+
stream: If True, yields content as delta chunks (streaming).
|
|
196
|
+
If False, yields content as complete text (non-streaming).
|
|
197
|
+
|
|
198
|
+
Yields:
|
|
199
|
+
StreamEvent objects for content, reasoning, reasoning_details, tool_call, tool_result,
|
|
200
|
+
metrics, and error.
|
|
201
|
+
"""
|
|
202
|
+
# 1. Setup Messages
|
|
203
|
+
current_messages = [m.model_copy() for m in messages]
|
|
204
|
+
|
|
205
|
+
# Add System Prompt
|
|
206
|
+
system_prompt = DEFAULT_SYSTEM_PROMPT
|
|
207
|
+
if self.agent_config.additional_system_prompt:
|
|
208
|
+
system_prompt += '\n' + self.agent_config.additional_system_prompt
|
|
209
|
+
|
|
210
|
+
inserted_system_prompt = False
|
|
211
|
+
if not current_messages or current_messages[0].role != "system":
|
|
212
|
+
current_messages.insert(0, Message(role="system", content=system_prompt))
|
|
213
|
+
inserted_system_prompt = True
|
|
214
|
+
else:
|
|
215
|
+
existing_system_prompt = current_messages[0].content or ""
|
|
216
|
+
if not existing_system_prompt.startswith(system_prompt):
|
|
217
|
+
separator = "\n\n" if existing_system_prompt else ""
|
|
218
|
+
current_messages[0].content = system_prompt + separator + existing_system_prompt
|
|
219
|
+
|
|
220
|
+
self._record_last_messages(current_messages, inserted_system_prompt)
|
|
221
|
+
|
|
222
|
+
iteration = 0
|
|
223
|
+
should_continue = True
|
|
224
|
+
previous_messages_count = len(current_messages)
|
|
225
|
+
|
|
226
|
+
while should_continue and iteration < self.config.max_iterations:
|
|
227
|
+
iteration += 1
|
|
228
|
+
|
|
229
|
+
# Prune history to save tokens if enabled
|
|
230
|
+
messages_to_send = current_messages
|
|
231
|
+
if self.config.enable_history_pruning:
|
|
232
|
+
messages_to_send = prune_tool_history(current_messages, previous_messages_count)
|
|
233
|
+
previous_messages_count = len(messages_to_send)
|
|
234
|
+
|
|
235
|
+
tool_calls: List[Dict[str, Any]] = []
|
|
236
|
+
content_accumulated = ""
|
|
237
|
+
reasoning_details_accumulated = []
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
if stream:
|
|
241
|
+
# Streaming mode: yield delta chunks
|
|
242
|
+
llm_stream = self.llm.chat_stream(
|
|
243
|
+
messages=messages_to_send,
|
|
244
|
+
tools=self.tools,
|
|
245
|
+
config=self.agent_config
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async for event in llm_stream:
|
|
249
|
+
if event.type in ["content", "reasoning"]:
|
|
250
|
+
yield event
|
|
251
|
+
if event.type == "content" and event.content:
|
|
252
|
+
content_accumulated += event.content
|
|
253
|
+
elif event.type == "reasoning_details":
|
|
254
|
+
# Accumulate reasoning_details for Gemini thought signatures
|
|
255
|
+
if event.details:
|
|
256
|
+
reasoning_details_accumulated.extend(event.details)
|
|
257
|
+
yield event
|
|
258
|
+
elif event.type == "tool_call":
|
|
259
|
+
tool_calls.append(event.tool_call)
|
|
260
|
+
yield event
|
|
261
|
+
elif event.type == "metrics":
|
|
262
|
+
yield event
|
|
263
|
+
else:
|
|
264
|
+
# Non-streaming mode: get complete response, yield as single event
|
|
265
|
+
response: ChatResponse = await self.llm.chat(
|
|
266
|
+
messages=messages_to_send,
|
|
267
|
+
tools=self.tools,
|
|
268
|
+
config=self.agent_config
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Yield complete content as single event
|
|
272
|
+
if response.content:
|
|
273
|
+
content_accumulated = response.content
|
|
274
|
+
yield StreamEvent(type="content", content=response.content)
|
|
275
|
+
|
|
276
|
+
# Capture reasoning_details for Gemini thought signatures
|
|
277
|
+
if response.reasoning_details:
|
|
278
|
+
reasoning_details_accumulated = response.reasoning_details
|
|
279
|
+
yield StreamEvent(type="reasoning_details", details=response.reasoning_details)
|
|
280
|
+
|
|
281
|
+
# Yield tool calls
|
|
282
|
+
if response.tool_calls:
|
|
283
|
+
for tc in response.tool_calls:
|
|
284
|
+
tool_calls.append(tc)
|
|
285
|
+
yield StreamEvent(type="tool_call", tool_call=tc)
|
|
286
|
+
|
|
287
|
+
# Yield metrics
|
|
288
|
+
if response.metrics:
|
|
289
|
+
yield StreamEvent(type="metrics", metrics=response.metrics)
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self._record_last_messages(current_messages, inserted_system_prompt)
|
|
293
|
+
yield self._llm_error_event(e)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Handle tool calls
|
|
297
|
+
if tool_calls:
|
|
298
|
+
current_messages.append(Message(
|
|
299
|
+
role="assistant",
|
|
300
|
+
content=content_accumulated if content_accumulated else None,
|
|
301
|
+
tool_calls=tool_calls,
|
|
302
|
+
# Preserve reasoning_details for Gemini thought signatures
|
|
303
|
+
reasoning_details=reasoning_details_accumulated if reasoning_details_accumulated else None
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
for tc in tool_calls:
|
|
307
|
+
func_name = tc["function"]["name"]
|
|
308
|
+
func_args_str = tc["function"]["arguments"]
|
|
309
|
+
call_id = tc["id"]
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
func_args = json.loads(func_args_str)
|
|
313
|
+
except json.JSONDecodeError:
|
|
314
|
+
error_msg = f"Invalid JSON arguments: {func_args_str}"
|
|
315
|
+
current_messages.append(Message(
|
|
316
|
+
role="tool",
|
|
317
|
+
tool_call_id=call_id,
|
|
318
|
+
name=func_name,
|
|
319
|
+
content=json.dumps({"error": error_msg})
|
|
320
|
+
))
|
|
321
|
+
yield StreamEvent(
|
|
322
|
+
type="tool_result",
|
|
323
|
+
tool_result={
|
|
324
|
+
"call_id": call_id,
|
|
325
|
+
"name": func_name,
|
|
326
|
+
"result": {"error": error_msg},
|
|
327
|
+
"is_error": True
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# Execute Tool
|
|
333
|
+
result, is_error, handled = await self.client.handle_tool_call(
|
|
334
|
+
func_name=func_name,
|
|
335
|
+
func_args=func_args,
|
|
336
|
+
session_id=self.session_id
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Handle extra tools if not a built-in Qveris tool
|
|
340
|
+
if not handled:
|
|
341
|
+
if self.extra_tool_handler:
|
|
342
|
+
try:
|
|
343
|
+
result = await self.extra_tool_handler(func_name, func_args)
|
|
344
|
+
is_error = False
|
|
345
|
+
except Exception as e:
|
|
346
|
+
result = {"error": str(e)}
|
|
347
|
+
is_error = True
|
|
348
|
+
else:
|
|
349
|
+
result = {"error": f"Unknown tool: {func_name}"}
|
|
350
|
+
is_error = True
|
|
351
|
+
|
|
352
|
+
# Yield tool_result event
|
|
353
|
+
yield StreamEvent(
|
|
354
|
+
type="tool_result",
|
|
355
|
+
tool_result={
|
|
356
|
+
"call_id": call_id,
|
|
357
|
+
"name": func_name,
|
|
358
|
+
"result": result,
|
|
359
|
+
"is_error": is_error
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
current_messages.append(Message(
|
|
364
|
+
role="tool",
|
|
365
|
+
tool_call_id=call_id,
|
|
366
|
+
name=func_name,
|
|
367
|
+
content=json.dumps(result, default=str)
|
|
368
|
+
))
|
|
369
|
+
|
|
370
|
+
self._record_last_messages(current_messages, inserted_system_prompt)
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
else:
|
|
374
|
+
if content_accumulated or reasoning_details_accumulated:
|
|
375
|
+
current_messages.append(Message(
|
|
376
|
+
role="assistant",
|
|
377
|
+
content=content_accumulated if content_accumulated else None,
|
|
378
|
+
reasoning_details=(
|
|
379
|
+
reasoning_details_accumulated if reasoning_details_accumulated else None
|
|
380
|
+
)
|
|
381
|
+
))
|
|
382
|
+
self._record_last_messages(current_messages, inserted_system_prompt)
|
|
383
|
+
should_continue = False
|
|
384
|
+
|
|
385
|
+
if should_continue:
|
|
386
|
+
self._record_last_messages(current_messages, inserted_system_prompt)
|
|
387
|
+
yield StreamEvent(
|
|
388
|
+
type="error",
|
|
389
|
+
error=f"Agent stopped after reaching max_iterations={self.config.max_iterations}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
async def run_to_completion(self, messages: List[Message]) -> str:
|
|
393
|
+
"""
|
|
394
|
+
Run the agent in non-streaming mode and return the final assistant text.
|
|
395
|
+
|
|
396
|
+
This is a convenience wrapper around `run(messages, stream=False)` that discards all events
|
|
397
|
+
except `content` and returns the concatenated text.
|
|
398
|
+
"""
|
|
399
|
+
content = ""
|
|
400
|
+
async for event in self.run(messages, stream=False):
|
|
401
|
+
if event.type == "content":
|
|
402
|
+
content += event.content or ""
|
|
403
|
+
elif event.type == "error":
|
|
404
|
+
raise RuntimeError(f"Agent execution failed: {event.error}")
|
|
405
|
+
return content
|
|
406
|
+
|
|
407
|
+
def new_session(self) -> str:
|
|
408
|
+
"""
|
|
409
|
+
Create and set a new session id.
|
|
410
|
+
|
|
411
|
+
The session id is forwarded to Qveris API calls (discover/call) and can be used server-side
|
|
412
|
+
for correlation, tracing, and analytics.
|
|
413
|
+
"""
|
|
414
|
+
self.session_id = str(uuid.uuid4())
|
|
415
|
+
return self.session_id
|
qveris/agent/memory.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from ..types import Message
|
|
4
|
+
|
|
5
|
+
def prune_tool_history(messages: List[Message], previous_messages_count: int) -> List[Message]:
|
|
6
|
+
"""
|
|
7
|
+
Collapses old discovery results into concise summaries to save tokens.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
messages: The full message history
|
|
11
|
+
previous_messages_count: The number of previous messages to prune
|
|
12
|
+
"""
|
|
13
|
+
# Map tool_call_id -> function_name from assistant messages
|
|
14
|
+
tool_id_to_name = {}
|
|
15
|
+
|
|
16
|
+
# First pass: Build map
|
|
17
|
+
for msg in messages:
|
|
18
|
+
if msg.role == 'assistant' and msg.tool_calls:
|
|
19
|
+
for tc in msg.tool_calls:
|
|
20
|
+
if tc.get('type') == 'function':
|
|
21
|
+
# Handle both dict and object access safely
|
|
22
|
+
func_name = tc.get('function', {}).get('name')
|
|
23
|
+
if not func_name:
|
|
24
|
+
continue
|
|
25
|
+
tool_id_to_name[tc['id']] = func_name
|
|
26
|
+
|
|
27
|
+
new_messages = []
|
|
28
|
+
for i, msg in enumerate(messages):
|
|
29
|
+
if msg.role == 'tool' and msg.tool_call_id and i < previous_messages_count:
|
|
30
|
+
tool_name = tool_id_to_name.get(msg.tool_call_id)
|
|
31
|
+
|
|
32
|
+
if tool_name in {'discover', 'search_tools'} and msg.content:
|
|
33
|
+
try:
|
|
34
|
+
content_obj = json.loads(msg.content)
|
|
35
|
+
# If it has 'results' array, it's the full uncompressed result
|
|
36
|
+
if isinstance(content_obj, dict) and 'results' in content_obj and isinstance(content_obj['results'], list):
|
|
37
|
+
# Create filtered content
|
|
38
|
+
tool_ids = [r.get('tool_id') or r.get('id') for r in content_obj['results']]
|
|
39
|
+
search_id = content_obj.get('search_id')
|
|
40
|
+
|
|
41
|
+
filtered_content = json.dumps({
|
|
42
|
+
"tool_ids": tool_ids,
|
|
43
|
+
"search_id": search_id
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# Create new message with filtered content
|
|
47
|
+
new_msg = msg.model_copy()
|
|
48
|
+
new_msg.content = filtered_content
|
|
49
|
+
new_messages.append(new_msg)
|
|
50
|
+
continue
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
new_messages.append(msg)
|
|
55
|
+
|
|
56
|
+
return new_messages
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .api import QverisClient
|
|
2
|
+
from .tools import (
|
|
3
|
+
CALL_TOOL_DEF,
|
|
4
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
5
|
+
DISCOVER_TOOL_DEF,
|
|
6
|
+
EXECUTE_TOOL_DEF,
|
|
7
|
+
GET_TOOLS_BY_IDS_TOOL_DEF,
|
|
8
|
+
INSPECT_TOOL_DEF,
|
|
9
|
+
SEARCH_TOOL_DEF,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"QverisClient",
|
|
14
|
+
"DEFAULT_SYSTEM_PROMPT",
|
|
15
|
+
"DISCOVER_TOOL_DEF",
|
|
16
|
+
"INSPECT_TOOL_DEF",
|
|
17
|
+
"CALL_TOOL_DEF",
|
|
18
|
+
"SEARCH_TOOL_DEF",
|
|
19
|
+
"GET_TOOLS_BY_IDS_TOOL_DEF",
|
|
20
|
+
"EXECUTE_TOOL_DEF",
|
|
21
|
+
]
|