evidor 1.10.2.dev1__tar.gz → 1.10.3.dev1__tar.gz
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.
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/CHANGELOG.md +19 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/PKG-INFO +42 -9
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/README.md +41 -8
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/pyproject.toml +1 -1
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/agent.py +26 -24
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/context.py +16 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/runner.py +33 -19
- evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/__init__.py +14 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/builder.py +120 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/parser.py +102 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/provider.py +245 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/stream.py +67 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/providers/base.py +2 -4
- evidor-1.10.3.dev1/src/evidor/agent/providers/client_pool.py +97 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/__init__.py +15 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/builder.py +243 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/parser.py +174 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/provider.py +198 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/stream.py +184 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/openai/__init__.py +15 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/openai/builder.py +92 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/openai/parser.py +145 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/openai/provider.py +217 -0
- evidor-1.10.3.dev1/src/evidor/agent/providers/openai/stream.py +57 -0
- evidor-1.10.3.dev1/src/evidor/constants.py +115 -0
- evidor-1.10.3.dev1/src/evidor/errors.py +46 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/client.py +8 -12
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/session.py +10 -7
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/thinking.py +12 -9
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/tool_executor.py +2 -4
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/tools.py +2 -1
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/execution.py +3 -5
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_agent.py +139 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_providers.py +249 -3
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_streaming.py +199 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_thinking.py +13 -17
- evidor-1.10.2.dev1/src/evidor/agent/providers/anthropic.py +0 -506
- evidor-1.10.2.dev1/src/evidor/agent/providers/gemini.py +0 -643
- evidor-1.10.2.dev1/src/evidor/agent/providers/openai.py +0 -510
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/.github/workflows/release.yml +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/.gitignore +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/AGENTS.md +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/CODE_OF_CONDUCT.md +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/LICENSE +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/requirements.txt +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/providers/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/config.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/runner.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/generation.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/streaming.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/context.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/events.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/runtime.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/semconv.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/sink.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/utils.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/calculator.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/__init__.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/schema.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_builtin_tools.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_conversation.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_filesystem_tools.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_langfuse_adapter.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_mcp.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_models.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_native_tools.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_opentelemetry_adapter.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_phoenix_adapter.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_prometheus_adapter.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_telemetry_events.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_telemetry_integration.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_tools.py +0 -0
- {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_web_search.py +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.10.3-dev.1 (2026-10-22)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **agent**: Fix gemini streaming issues
|
|
10
|
+
([`f9d64f2`](https://github.com/vedangiitb/evidor-core/commit/f9d64f25f195c97c3ee5dd01c18b1d852e92fb70))
|
|
11
|
+
|
|
12
|
+
- **agent**: Fixing gemini request building, response parsing and adapter lifecycle issues
|
|
13
|
+
([`a67efbe`](https://github.com/vedangiitb/evidor-core/commit/a67efbecc0c9331fc61a14ddbc7512b656ae5e95))
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
- **agent**: Docs update for stateless sessions update
|
|
18
|
+
([`57569f8`](https://github.com/vedangiitb/evidor-core/commit/57569f843aa7ceeece9f47ac3abec5cfe48a175d))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## v1.10.2 (2026-10-21)
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
## v1.10.2-dev.1 (2026-10-21)
|
|
6
25
|
|
|
7
26
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evidor
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.3-dev.1
|
|
4
4
|
Summary: A small, provider-agnostic harness for LLM input and output.
|
|
5
5
|
Project-URL: Homepage, https://github.com/evidor/evidor-core
|
|
6
6
|
Project-URL: Repository, https://github.com/evidor/evidor-core
|
|
@@ -108,12 +108,13 @@ response = agent.send("Give a one-sentence explanation of dependency inversion."
|
|
|
108
108
|
print(response.text)
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
## Core Architecture:
|
|
111
|
+
## Core Architecture: Execution Paradigms
|
|
112
112
|
|
|
113
|
-
Evidor cleanly separates AI execution into
|
|
113
|
+
Evidor cleanly separates AI execution into flexible paradigms:
|
|
114
114
|
|
|
115
|
-
1. **Stateful Conversations (`Agent`) (Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
|
|
116
|
-
2. **Stateless
|
|
115
|
+
1. **Stateful Conversations (`Agent`) (Default & Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
|
|
116
|
+
2. **Stateless Agents (`Agent(..., stateless=True)`)**: When you want high-level agent capabilities (automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry) but want every `.send()` / `.send_async()` call to execute completely independently without remembering past turns.
|
|
117
|
+
3. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
|
|
117
118
|
|
|
118
119
|
---
|
|
119
120
|
|
|
@@ -231,6 +232,38 @@ agent.reset() # or agent.clear_history()
|
|
|
231
232
|
print(agent.last_response_id) # None
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
### Stateless Agent (`stateless=True`)
|
|
236
|
+
|
|
237
|
+
If you want the high-level capabilities of an `Agent`—automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry—but want each `.send()` or `.send_async()` call to execute independently without remembering prior turns, pass `stateless=True`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from evidor import Agent, OpenAIProvider
|
|
241
|
+
from evidor.tools import tool
|
|
242
|
+
|
|
243
|
+
@tool
|
|
244
|
+
def get_stock_price(ticker: str) -> float:
|
|
245
|
+
return 150.25
|
|
246
|
+
|
|
247
|
+
# Each send() call executes independently without cross-turn memory
|
|
248
|
+
agent = Agent(
|
|
249
|
+
OpenAIProvider(model="gpt-6-astra"),
|
|
250
|
+
system_prompt="You are a financial assistant.",
|
|
251
|
+
tools=[get_stock_price],
|
|
252
|
+
stateless=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Turn 1: Invokes tool loop and returns answer
|
|
256
|
+
resp1 = agent.send("What is AAPL's price?")
|
|
257
|
+
|
|
258
|
+
# Turn 2: Starts completely fresh without prior turns or response chaining
|
|
259
|
+
resp2 = agent.send("What did I just ask?")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Key Advantages of `stateless=True`**:
|
|
263
|
+
- **Automatic Inter-turn Isolation**: For OpenAI and Gemini, `previous_response_id` / `previous_interaction_id` are not chained across turns. For Anthropic, earlier turns are not replayed.
|
|
264
|
+
- **Intra-turn Tool Loops Intact**: Multi-step tool calls *within* a single turn (tool call $\to$ tool result $\to$ final response) work seamlessly because state is preserved during the turn.
|
|
265
|
+
- **Concurrency-Safe**: Each turn runs within an isolated ephemeral context frame, enabling concurrent `.send_async()` calls without cross-talk.
|
|
266
|
+
|
|
234
267
|
---
|
|
235
268
|
|
|
236
269
|
## 2. Stateless LLM Calls
|
|
@@ -514,15 +547,15 @@ provider = AnthropicProvider(
|
|
|
514
547
|
thinking=AnthropicThinkingConfig(budget_tokens=4096),
|
|
515
548
|
)
|
|
516
549
|
|
|
517
|
-
# 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high')
|
|
550
|
+
# 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') and summaries ('auto', 'none')
|
|
518
551
|
# Shorthand string (Gemini 3.x thinking_level):
|
|
519
552
|
provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
|
|
520
|
-
# Shorthand bool (False disables thinking; True enables
|
|
553
|
+
# Shorthand bool (False disables thinking; True enables default thinking):
|
|
521
554
|
provider = GeminiProvider(model="gemini-3.8-flash", thinking=False)
|
|
522
555
|
# Or via typed config:
|
|
523
556
|
provider = GeminiProvider(
|
|
524
557
|
model="gemini-3.8-flash",
|
|
525
|
-
thinking=GeminiThinkingConfig(thinking_level="high",
|
|
558
|
+
thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
|
|
526
559
|
)
|
|
527
560
|
```
|
|
528
561
|
|
|
@@ -1253,7 +1286,7 @@ Supported providers, example models, and reasoning/thinking support:
|
|
|
1253
1286
|
| --- | --- | --- | --- | --- |
|
|
1254
1287
|
| `OpenAIProvider` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`, `gpt-4.1-mini` | `reasoning_effort` (`"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`) | `OPENAI_API_KEY` | `evidor[openai]` |
|
|
1255
1288
|
| `AnthropicProvider` | `claude-3-7-sonnet` | Extended budget (`budget_tokens >= 1024`) or adaptive | `ANTHROPIC_API_KEY` | `evidor[anthropic]` |
|
|
1256
|
-
| `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`)
|
|
1289
|
+
| `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`), `thinking_summaries` (`"auto"`, `"none"`) | `GEMINI_API_KEY` | `evidor[gemini]` |
|
|
1257
1290
|
|
|
1258
1291
|
API keys can be supplied explicitly via the `api_key` constructor argument or read automatically from environment variables. Custom base URLs for OpenAI-compatible providers (such as DeepSeek) can be supplied via the `base_url` constructor argument or `OPENAI_BASE_URL` environment variable:
|
|
1259
1292
|
|
|
@@ -21,12 +21,13 @@ response = agent.send("Give a one-sentence explanation of dependency inversion."
|
|
|
21
21
|
print(response.text)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## Core Architecture:
|
|
24
|
+
## Core Architecture: Execution Paradigms
|
|
25
25
|
|
|
26
|
-
Evidor cleanly separates AI execution into
|
|
26
|
+
Evidor cleanly separates AI execution into flexible paradigms:
|
|
27
27
|
|
|
28
|
-
1. **Stateful Conversations (`Agent`) (Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
|
|
29
|
-
2. **Stateless
|
|
28
|
+
1. **Stateful Conversations (`Agent`) (Default & Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
|
|
29
|
+
2. **Stateless Agents (`Agent(..., stateless=True)`)**: When you want high-level agent capabilities (automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry) but want every `.send()` / `.send_async()` call to execute completely independently without remembering past turns.
|
|
30
|
+
3. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -144,6 +145,38 @@ agent.reset() # or agent.clear_history()
|
|
|
144
145
|
print(agent.last_response_id) # None
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
### Stateless Agent (`stateless=True`)
|
|
149
|
+
|
|
150
|
+
If you want the high-level capabilities of an `Agent`—automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry—but want each `.send()` or `.send_async()` call to execute independently without remembering prior turns, pass `stateless=True`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from evidor import Agent, OpenAIProvider
|
|
154
|
+
from evidor.tools import tool
|
|
155
|
+
|
|
156
|
+
@tool
|
|
157
|
+
def get_stock_price(ticker: str) -> float:
|
|
158
|
+
return 150.25
|
|
159
|
+
|
|
160
|
+
# Each send() call executes independently without cross-turn memory
|
|
161
|
+
agent = Agent(
|
|
162
|
+
OpenAIProvider(model="gpt-6-astra"),
|
|
163
|
+
system_prompt="You are a financial assistant.",
|
|
164
|
+
tools=[get_stock_price],
|
|
165
|
+
stateless=True,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Turn 1: Invokes tool loop and returns answer
|
|
169
|
+
resp1 = agent.send("What is AAPL's price?")
|
|
170
|
+
|
|
171
|
+
# Turn 2: Starts completely fresh without prior turns or response chaining
|
|
172
|
+
resp2 = agent.send("What did I just ask?")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Key Advantages of `stateless=True`**:
|
|
176
|
+
- **Automatic Inter-turn Isolation**: For OpenAI and Gemini, `previous_response_id` / `previous_interaction_id` are not chained across turns. For Anthropic, earlier turns are not replayed.
|
|
177
|
+
- **Intra-turn Tool Loops Intact**: Multi-step tool calls *within* a single turn (tool call $\to$ tool result $\to$ final response) work seamlessly because state is preserved during the turn.
|
|
178
|
+
- **Concurrency-Safe**: Each turn runs within an isolated ephemeral context frame, enabling concurrent `.send_async()` calls without cross-talk.
|
|
179
|
+
|
|
147
180
|
---
|
|
148
181
|
|
|
149
182
|
## 2. Stateless LLM Calls
|
|
@@ -427,15 +460,15 @@ provider = AnthropicProvider(
|
|
|
427
460
|
thinking=AnthropicThinkingConfig(budget_tokens=4096),
|
|
428
461
|
)
|
|
429
462
|
|
|
430
|
-
# 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high')
|
|
463
|
+
# 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') and summaries ('auto', 'none')
|
|
431
464
|
# Shorthand string (Gemini 3.x thinking_level):
|
|
432
465
|
provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
|
|
433
|
-
# Shorthand bool (False disables thinking; True enables
|
|
466
|
+
# Shorthand bool (False disables thinking; True enables default thinking):
|
|
434
467
|
provider = GeminiProvider(model="gemini-3.8-flash", thinking=False)
|
|
435
468
|
# Or via typed config:
|
|
436
469
|
provider = GeminiProvider(
|
|
437
470
|
model="gemini-3.8-flash",
|
|
438
|
-
thinking=GeminiThinkingConfig(thinking_level="high",
|
|
471
|
+
thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
|
|
439
472
|
)
|
|
440
473
|
```
|
|
441
474
|
|
|
@@ -1166,7 +1199,7 @@ Supported providers, example models, and reasoning/thinking support:
|
|
|
1166
1199
|
| --- | --- | --- | --- | --- |
|
|
1167
1200
|
| `OpenAIProvider` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`, `gpt-4.1-mini` | `reasoning_effort` (`"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`) | `OPENAI_API_KEY` | `evidor[openai]` |
|
|
1168
1201
|
| `AnthropicProvider` | `claude-3-7-sonnet` | Extended budget (`budget_tokens >= 1024`) or adaptive | `ANTHROPIC_API_KEY` | `evidor[anthropic]` |
|
|
1169
|
-
| `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`)
|
|
1202
|
+
| `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`), `thinking_summaries` (`"auto"`, `"none"`) | `GEMINI_API_KEY` | `evidor[gemini]` |
|
|
1170
1203
|
|
|
1171
1204
|
API keys can be supplied explicitly via the `api_key` constructor argument or read automatically from environment variables. Custom base URLs for OpenAI-compatible providers (such as DeepSeek) can be supplied via the `base_url` constructor argument or `OPENAI_BASE_URL` environment variable:
|
|
1172
1205
|
|
|
@@ -7,6 +7,12 @@ from collections.abc import Callable, Sequence
|
|
|
7
7
|
import threading
|
|
8
8
|
from typing import Any, Literal, overload
|
|
9
9
|
|
|
10
|
+
from evidor.constants import (
|
|
11
|
+
AGENT_CLOSE_SYNC_ERROR,
|
|
12
|
+
AGENT_SEND_SYNC_ERROR,
|
|
13
|
+
AGENT_STREAM_SYNC_ERROR,
|
|
14
|
+
AGENT_SYNC_MCP_TOOLS_ERROR,
|
|
15
|
+
)
|
|
10
16
|
from evidor.models import (
|
|
11
17
|
AsyncStreamResponse,
|
|
12
18
|
GenerationResponse,
|
|
@@ -48,7 +54,15 @@ class Agent:
|
|
|
48
54
|
store: bool | None = None,
|
|
49
55
|
compact_threshold: int | None = None,
|
|
50
56
|
telemetry: TelemetryRuntime | TelemetrySink | Sequence[TelemetrySink] | None = None,
|
|
57
|
+
stateless: bool = False,
|
|
51
58
|
) -> None:
|
|
59
|
+
if stateless:
|
|
60
|
+
if previous_response_id is not None:
|
|
61
|
+
raise ValueError("Cannot configure previous_response_id on a stateless Agent.")
|
|
62
|
+
if conversation_id is not None:
|
|
63
|
+
raise ValueError("Cannot configure conversation_id on a stateless Agent.")
|
|
64
|
+
|
|
65
|
+
self._stateless = stateless
|
|
52
66
|
self._provider = provider
|
|
53
67
|
self._system_prompt = system_prompt
|
|
54
68
|
self._compact_threshold = compact_threshold or getattr(provider, "compact_threshold", None)
|
|
@@ -93,6 +107,7 @@ class Agent:
|
|
|
93
107
|
tool_executor=self._tool_executor,
|
|
94
108
|
telemetry_bridge=self._telemetry_bridge,
|
|
95
109
|
max_tool_iterations=max_tool_iterations,
|
|
110
|
+
stateless=self._stateless,
|
|
96
111
|
)
|
|
97
112
|
|
|
98
113
|
# Background event loop and thread synchronization
|
|
@@ -117,6 +132,11 @@ class Agent:
|
|
|
117
132
|
"""
|
|
118
133
|
return getattr(self._provider, "context_strategy", "stateless_replay")
|
|
119
134
|
|
|
135
|
+
@property
|
|
136
|
+
def stateless(self) -> bool:
|
|
137
|
+
"""Whether this agent runs each turn statelessly without retaining conversation memory across turns."""
|
|
138
|
+
return self._stateless
|
|
139
|
+
|
|
120
140
|
@property
|
|
121
141
|
def last_response_id(self) -> str | None:
|
|
122
142
|
"""The identifier of the last response or interaction in this conversation."""
|
|
@@ -192,10 +212,7 @@ class Agent:
|
|
|
192
212
|
return
|
|
193
213
|
self._sync_loop.run(
|
|
194
214
|
lambda: self._sync_mcp_tools_async(force=force),
|
|
195
|
-
error_message=
|
|
196
|
-
"Cannot synchronize MCP tools synchronously from a running event loop. "
|
|
197
|
-
"Access agent.tools outside an event loop or run an async agent turn first."
|
|
198
|
-
),
|
|
215
|
+
error_message=AGENT_SYNC_MCP_TOOLS_ERROR,
|
|
199
216
|
)
|
|
200
217
|
|
|
201
218
|
async def _sync_mcp_tools_async(self, force: bool = False) -> None:
|
|
@@ -230,16 +247,10 @@ class Agent:
|
|
|
230
247
|
with self._lock:
|
|
231
248
|
return self._sync_loop.run(
|
|
232
249
|
lambda: self.send_async(message, stream=False),
|
|
233
|
-
error_message=
|
|
234
|
-
"Agent.send() cannot be called from a running event loop. "
|
|
235
|
-
"Use 'await agent.send_async()' instead."
|
|
236
|
-
),
|
|
250
|
+
error_message=AGENT_SEND_SYNC_ERROR,
|
|
237
251
|
)
|
|
238
252
|
|
|
239
|
-
check_sync_allowed(
|
|
240
|
-
"Agent.send(..., stream=True) cannot be called from a running event loop. "
|
|
241
|
-
"Use 'await agent.send_async(..., stream=True)' instead."
|
|
242
|
-
)
|
|
253
|
+
check_sync_allowed(AGENT_STREAM_SYNC_ERROR)
|
|
243
254
|
with self._lock:
|
|
244
255
|
bridge = sync_stream_bridge(
|
|
245
256
|
self._sync_loop, lambda: self.send_async(message, stream=True)
|
|
@@ -282,10 +293,7 @@ class Agent:
|
|
|
282
293
|
if self._background_loop is not None and self._background_loop.is_running:
|
|
283
294
|
self._background_loop.run(
|
|
284
295
|
self._close_async_resources,
|
|
285
|
-
error_message=
|
|
286
|
-
"Agent.close() cannot be called from a running event loop. "
|
|
287
|
-
"Use 'await agent.close_async()' or 'async with agent:' instead."
|
|
288
|
-
),
|
|
296
|
+
error_message=AGENT_CLOSE_SYNC_ERROR,
|
|
289
297
|
)
|
|
290
298
|
else:
|
|
291
299
|
running_loop = None
|
|
@@ -296,18 +304,12 @@ class Agent:
|
|
|
296
304
|
|
|
297
305
|
if running_loop is not None:
|
|
298
306
|
if self._mcp_client is not None:
|
|
299
|
-
check_sync_allowed(
|
|
300
|
-
"Agent.close() cannot be called from a running event loop. "
|
|
301
|
-
"Use 'await agent.close_async()' or 'async with agent:' instead."
|
|
302
|
-
)
|
|
307
|
+
check_sync_allowed(AGENT_CLOSE_SYNC_ERROR)
|
|
303
308
|
running_loop.create_task(self._close_async_resources())
|
|
304
309
|
else:
|
|
305
310
|
run_async_in_sync(
|
|
306
311
|
self._close_async_resources,
|
|
307
|
-
error_message=
|
|
308
|
-
"Agent.close() cannot be called from a running event loop. "
|
|
309
|
-
"Use 'await agent.close_async()' or 'async with agent:' instead."
|
|
310
|
-
),
|
|
312
|
+
error_message=AGENT_CLOSE_SYNC_ERROR,
|
|
311
313
|
)
|
|
312
314
|
self._close_sync_resources()
|
|
313
315
|
finally:
|
|
@@ -50,6 +50,22 @@ class ConversationContext:
|
|
|
50
50
|
if self.system_prompt:
|
|
51
51
|
self._messages.append(Message(role="system", content=self.system_prompt))
|
|
52
52
|
|
|
53
|
+
def spawn_turn_context(self) -> ConversationContext:
|
|
54
|
+
"""Create a fresh isolated turn context with the same configuration."""
|
|
55
|
+
return ConversationContext(
|
|
56
|
+
strategy=self.strategy,
|
|
57
|
+
system_prompt=self.system_prompt,
|
|
58
|
+
previous_response_id=None,
|
|
59
|
+
conversation_id=None,
|
|
60
|
+
store=self.store,
|
|
61
|
+
compact_threshold=self.compact_threshold,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def copy_from(self, other: ConversationContext) -> None:
|
|
65
|
+
"""Update messages and last_response_id from another turn context."""
|
|
66
|
+
self._messages = list(other._messages)
|
|
67
|
+
self.last_response_id = other.last_response_id
|
|
68
|
+
|
|
53
69
|
def append_user_message(self, message: str) -> Message:
|
|
54
70
|
"""Record a new user message in the conversation history."""
|
|
55
71
|
msg = Message(role="user", content=message)
|
|
@@ -38,12 +38,14 @@ class TurnExecutor:
|
|
|
38
38
|
tool_executor: ToolExecutor,
|
|
39
39
|
telemetry_bridge: AgentTelemetryBridge,
|
|
40
40
|
max_tool_iterations: int = 10,
|
|
41
|
+
stateless: bool = False,
|
|
41
42
|
) -> None:
|
|
42
43
|
self._provider = provider
|
|
43
44
|
self._context = context
|
|
44
45
|
self._tool_executor = tool_executor
|
|
45
46
|
self._telemetry_bridge = telemetry_bridge
|
|
46
47
|
self._max_tool_iterations = max_tool_iterations
|
|
48
|
+
self._stateless = stateless
|
|
47
49
|
|
|
48
50
|
def update_tool_executor(self, executor: ToolExecutor) -> None:
|
|
49
51
|
"""Update the active ToolExecutor (e.g. after MCP tool synchronization)."""
|
|
@@ -78,8 +80,10 @@ class TurnExecutor:
|
|
|
78
80
|
self,
|
|
79
81
|
final_response: GenerationResponse,
|
|
80
82
|
last_response: GenerationResponse | None,
|
|
83
|
+
context: ConversationContext | None = None,
|
|
81
84
|
) -> GenerationResponse:
|
|
82
85
|
"""Synthesize a fallback response if the model exhausts tool iterations without concluding."""
|
|
86
|
+
ctx = context or self._context
|
|
83
87
|
final_text = (final_response.text or "").strip()
|
|
84
88
|
if not final_text and last_response:
|
|
85
89
|
final_text = (last_response.text or "").strip()
|
|
@@ -87,7 +91,7 @@ class TurnExecutor:
|
|
|
87
91
|
final_text = (
|
|
88
92
|
f"Reached maximum tool iterations ({self._max_tool_iterations}) without a final response."
|
|
89
93
|
)
|
|
90
|
-
|
|
94
|
+
ctx.append_assistant_response(text=final_text)
|
|
91
95
|
return GenerationResponse(
|
|
92
96
|
id=final_response.id or (last_response.id if last_response else None),
|
|
93
97
|
text=final_text,
|
|
@@ -96,20 +100,21 @@ class TurnExecutor:
|
|
|
96
100
|
|
|
97
101
|
async def run_turn_async(self, message: str) -> GenerationResponse:
|
|
98
102
|
"""Asynchronously advance one complete user turn with automatic tool calling."""
|
|
103
|
+
turn_ctx = self._context.spawn_turn_context() if self._stateless else self._context
|
|
99
104
|
run_id = f"run_{secrets.token_hex(6)}"
|
|
100
105
|
with self._telemetry_bridge.run_scope(
|
|
101
|
-
run_id, message,
|
|
106
|
+
run_id, message, turn_ctx.system_prompt
|
|
102
107
|
) as root_ctx:
|
|
103
108
|
t0 = time.perf_counter()
|
|
104
109
|
iterations = 0
|
|
105
110
|
try:
|
|
106
|
-
user_msg =
|
|
111
|
+
user_msg = turn_ctx.append_user_message(message)
|
|
107
112
|
current_turn_messages: Sequence[Message] = [user_msg]
|
|
108
113
|
response: GenerationResponse | None = None
|
|
109
114
|
|
|
110
115
|
for _ in range(self._max_tool_iterations):
|
|
111
116
|
iterations += 1
|
|
112
|
-
req =
|
|
117
|
+
req = turn_ctx.build_generation_request(
|
|
113
118
|
current_turn_messages, self._tool_executor.tools
|
|
114
119
|
)
|
|
115
120
|
|
|
@@ -117,9 +122,9 @@ class TurnExecutor:
|
|
|
117
122
|
self._provider, req
|
|
118
123
|
)
|
|
119
124
|
if response.id:
|
|
120
|
-
|
|
125
|
+
turn_ctx.last_response_id = response.id
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
turn_ctx.append_assistant_response(
|
|
123
128
|
text=response.text,
|
|
124
129
|
tool_calls=response.tool_calls,
|
|
125
130
|
thinking=response.thinking,
|
|
@@ -128,6 +133,8 @@ class TurnExecutor:
|
|
|
128
133
|
)
|
|
129
134
|
|
|
130
135
|
if not response.tool_calls:
|
|
136
|
+
if self._stateless:
|
|
137
|
+
self._context.copy_from(turn_ctx)
|
|
131
138
|
duration_ms = (time.perf_counter() - t0) * 1000
|
|
132
139
|
self._telemetry_bridge.emit_run_end(
|
|
133
140
|
root_ctx=root_ctx,
|
|
@@ -141,19 +148,21 @@ class TurnExecutor:
|
|
|
141
148
|
|
|
142
149
|
# Execute tool calls and record results
|
|
143
150
|
tool_results = await self.execute_tool_calls_async(response.tool_calls)
|
|
144
|
-
|
|
151
|
+
turn_ctx.append_tool_results(tool_results)
|
|
145
152
|
current_turn_messages = tool_results
|
|
146
153
|
|
|
147
154
|
# Iterations exhausted: perform terminal synthesis without tools
|
|
148
155
|
iterations += 1
|
|
149
|
-
final_req =
|
|
156
|
+
final_req = turn_ctx.build_final_request(current_turn_messages)
|
|
150
157
|
final_response = await self._telemetry_bridge.generate_with_telemetry_async(
|
|
151
158
|
self._provider, final_req
|
|
152
159
|
)
|
|
153
160
|
if final_response.id:
|
|
154
|
-
|
|
161
|
+
turn_ctx.last_response_id = final_response.id
|
|
155
162
|
|
|
156
|
-
result = self.finalize_loop_expiry(final_response, response)
|
|
163
|
+
result = self.finalize_loop_expiry(final_response, response, turn_ctx)
|
|
164
|
+
if self._stateless:
|
|
165
|
+
self._context.copy_from(turn_ctx)
|
|
157
166
|
duration_ms = (time.perf_counter() - t0) * 1000
|
|
158
167
|
self._telemetry_bridge.emit_run_end(
|
|
159
168
|
root_ctx=root_ctx,
|
|
@@ -179,20 +188,21 @@ class TurnExecutor:
|
|
|
179
188
|
|
|
180
189
|
async def stream_turn_async(self, message: str) -> AsyncIterator[StreamChunk]:
|
|
181
190
|
"""Asynchronously stream chunks across multi-turn tool execution."""
|
|
191
|
+
turn_ctx = self._context.spawn_turn_context() if self._stateless else self._context
|
|
182
192
|
run_id = f"run_{secrets.token_hex(6)}"
|
|
183
193
|
with self._telemetry_bridge.run_scope(
|
|
184
|
-
run_id, message,
|
|
194
|
+
run_id, message, turn_ctx.system_prompt
|
|
185
195
|
) as root_ctx:
|
|
186
196
|
t0 = time.perf_counter()
|
|
187
197
|
iterations = 0
|
|
188
198
|
try:
|
|
189
|
-
user_msg =
|
|
199
|
+
user_msg = turn_ctx.append_user_message(message)
|
|
190
200
|
current_turn_messages: Sequence[Message] = [user_msg]
|
|
191
201
|
response: GenerationResponse | None = None
|
|
192
202
|
|
|
193
203
|
for _ in range(self._max_tool_iterations):
|
|
194
204
|
iterations += 1
|
|
195
|
-
req =
|
|
205
|
+
req = turn_ctx.build_generation_request(
|
|
196
206
|
current_turn_messages, self._tool_executor.tools, stream=True
|
|
197
207
|
)
|
|
198
208
|
|
|
@@ -207,9 +217,9 @@ class TurnExecutor:
|
|
|
207
217
|
response = await turn_stream.get_final_response()
|
|
208
218
|
|
|
209
219
|
if response.id:
|
|
210
|
-
|
|
220
|
+
turn_ctx.last_response_id = response.id
|
|
211
221
|
|
|
212
|
-
|
|
222
|
+
turn_ctx.append_assistant_response(
|
|
213
223
|
text=response.text,
|
|
214
224
|
tool_calls=response.tool_calls,
|
|
215
225
|
thinking=response.thinking,
|
|
@@ -218,6 +228,8 @@ class TurnExecutor:
|
|
|
218
228
|
)
|
|
219
229
|
|
|
220
230
|
if not response.tool_calls:
|
|
231
|
+
if self._stateless:
|
|
232
|
+
self._context.copy_from(turn_ctx)
|
|
221
233
|
duration_ms = (time.perf_counter() - t0) * 1000
|
|
222
234
|
self._telemetry_bridge.emit_run_end(
|
|
223
235
|
root_ctx=root_ctx,
|
|
@@ -231,12 +243,12 @@ class TurnExecutor:
|
|
|
231
243
|
|
|
232
244
|
# Execute tool calls asynchronously
|
|
233
245
|
tool_results = await self.execute_tool_calls_async(response.tool_calls)
|
|
234
|
-
|
|
246
|
+
turn_ctx.append_tool_results(tool_results)
|
|
235
247
|
current_turn_messages = tool_results
|
|
236
248
|
|
|
237
249
|
# Iterations exhausted: stream final synthesis without tools
|
|
238
250
|
iterations += 1
|
|
239
|
-
final_req =
|
|
251
|
+
final_req = turn_ctx.build_final_request(current_turn_messages, stream=True)
|
|
240
252
|
final_stream = await self._telemetry_bridge.generate_with_telemetry_stream_async(
|
|
241
253
|
self._provider, final_req
|
|
242
254
|
)
|
|
@@ -247,9 +259,11 @@ class TurnExecutor:
|
|
|
247
259
|
if final_response is None:
|
|
248
260
|
final_response = await final_stream.get_final_response()
|
|
249
261
|
if final_response.id:
|
|
250
|
-
|
|
262
|
+
turn_ctx.last_response_id = final_response.id
|
|
251
263
|
|
|
252
|
-
result = self.finalize_loop_expiry(final_response, response)
|
|
264
|
+
result = self.finalize_loop_expiry(final_response, response, turn_ctx)
|
|
265
|
+
if self._stateless:
|
|
266
|
+
self._context.copy_from(turn_ctx)
|
|
253
267
|
duration_ms = (time.perf_counter() - t0) * 1000
|
|
254
268
|
self._telemetry_bridge.emit_run_end(
|
|
255
269
|
root_ctx=root_ctx,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Anthropic provider package."""
|
|
2
|
+
|
|
3
|
+
from .builder import AnthropicPayloadBuilder
|
|
4
|
+
from .parser import AnthropicResponseParser
|
|
5
|
+
from .provider import AnthropicProvider
|
|
6
|
+
from .stream import AnthropicStreamHandler
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AnthropicPayloadBuilder",
|
|
10
|
+
"AnthropicProvider",
|
|
11
|
+
"AnthropicResponseParser",
|
|
12
|
+
"AnthropicStreamHandler",
|
|
13
|
+
]
|
|
14
|
+
|