evidor 1.11.0.dev1__tar.gz → 1.11.1.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.11.0.dev1 → evidor-1.11.1.dev1}/CHANGELOG.md +14 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/PKG-INFO +14 -7
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/README.md +13 -6
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/pyproject.toml +1 -1
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/builder.py +115 -28
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/parser.py +24 -5
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/stream.py +16 -5
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/parser.py +4 -4
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_thinking.py +193 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/.github/workflows/release.yml +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/.gitignore +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/AGENTS.md +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/CODE_OF_CONDUCT.md +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/LICENSE +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/requirements.txt +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/agent.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/context.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/runner.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/provider.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/base.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/client_pool.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/builder.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/provider.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/stream.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/builder.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/parser.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/provider.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/stream.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/constants.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/errors.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/client.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/config.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/runner.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/session.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/generation.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/streaming.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/thinking.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/context.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/events.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/runtime.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/semconv.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/sink.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/tool_executor.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/tools.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/utils.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/calculator.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/__init__.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/execution.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/schema.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_agent.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_builtin_tools.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_conversation.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_filesystem_tools.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_langfuse_adapter.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_mcp.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_models.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_native_tools.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_opentelemetry_adapter.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_phoenix_adapter.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_prometheus_adapter.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_providers.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_streaming.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_telemetry_events.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_telemetry_integration.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_tools.py +0 -0
- {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_web_search.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.11.1-dev.1 (2026-11-04)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **anthropic**: Fixing reasoning mode issues
|
|
10
|
+
([`405c681`](https://github.com/vedangiitb/evidor-core/commit/405c68150ea149f0bb693a1fb23bcd5151267211))
|
|
11
|
+
|
|
12
|
+
- **gemini**: Removing redundant fields in parsing
|
|
13
|
+
([`555be1d`](https://github.com/vedangiitb/evidor-core/commit/555be1dfd2e0f79cc5c27c3c9c6cf342f5ca1029))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## v1.11.0 (2026-11-03)
|
|
17
|
+
|
|
18
|
+
|
|
5
19
|
## v1.11.0-dev.1 (2026-11-03)
|
|
6
20
|
|
|
7
21
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evidor
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.1-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
|
|
@@ -554,10 +554,17 @@ p_openai_cfg = OpenAIProvider(
|
|
|
554
554
|
thinking=ThinkingConfig(effort="high", summary="auto"),
|
|
555
555
|
)
|
|
556
556
|
|
|
557
|
-
# 2. Anthropic: Configure
|
|
558
|
-
|
|
557
|
+
# 2. Anthropic: Configure adaptive thinking with reasoning summary, or manual budget tokens
|
|
558
|
+
# Adaptive thinking with reasoning summary:
|
|
559
|
+
p_anthropic_adaptive = AnthropicProvider(
|
|
559
560
|
model="claude-3-7-sonnet-20250219",
|
|
560
|
-
thinking=ThinkingConfig(
|
|
561
|
+
thinking=ThinkingConfig(effort="high", summary="auto"),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Extended manual budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed):
|
|
565
|
+
p_anthropic_budget = AnthropicProvider(
|
|
566
|
+
model="claude-3-7-sonnet-20250219",
|
|
567
|
+
thinking=ThinkingConfig(budget_tokens=4096, summary="omitted"),
|
|
561
568
|
)
|
|
562
569
|
|
|
563
570
|
# 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
|
|
@@ -577,9 +584,9 @@ p_gemini_budget = GeminiProvider(
|
|
|
577
584
|
|
|
578
585
|
| Field | Type | Description & Provider Mapping |
|
|
579
586
|
| :--- | :--- | :--- |
|
|
580
|
-
| `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`effort`). If an unexpected effort level is passed, the provider API handles it directly. |
|
|
587
|
+
| `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`output_config.effort` with adaptive thinking). If an unexpected effort level is passed, the provider API handles it directly. |
|
|
581
588
|
| `budget_tokens` | `int \| None` | Maximum reasoning token budget where supported. Supported by Anthropic (`thinking={"type": "enabled", "budget_tokens": ...}`) and Gemini (`thinking_budget`). |
|
|
582
|
-
| `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported
|
|
589
|
+
| `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported across all three providers:<br>• **Anthropic**: Controls reasoning exposure via Claude's `thinking.display` parameter (`"summarized"` vs `"omitted"`). Values such as `"auto"`, `"summarized"`, `"concise"`, or `"detailed"` set `display: "summarized"`, populating `response.thinking` with a human-readable summary. Values like `"none"` or `"omitted"` set `display: "omitted"`, omitting thinking text to minimize time-to-first-token while preserving cryptographic signatures in `thought_blocks` for multi-turn replay.<br>• **OpenAI**: Configures `reasoning.summary`.<br>• **Gemini**: Configures `thinking_summaries` (`"auto"` or `"none"`). As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
|
|
583
590
|
|
|
584
591
|
> [!NOTE]
|
|
585
592
|
> **No Boolean or Standalone Integer Shorthands**: Shorthands like `thinking=False` or `thinking=4096` are intentionally not supported to avoid ambiguity. To run without thinking, simply omit the parameter or pass `thinking=None`. To configure a token budget, use `ThinkingConfig(budget_tokens=4096)`.
|
|
@@ -606,7 +613,7 @@ if response.usage and response.usage.reasoning_tokens:
|
|
|
606
613
|
|
|
607
614
|
#### Multi-Turn Tool Calling & Cryptographic Signatures
|
|
608
615
|
|
|
609
|
-
Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling.
|
|
616
|
+
Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling. Furthermore, Evidor normalizes and sanitizes thought blocks across providers (e.g. converting between Gemini's and Anthropic's signature representations and stripping non-standard fields) so history can be transferred or replayed seamlessly without schema violations.
|
|
610
617
|
|
|
611
618
|
## Tools & Function Calling
|
|
612
619
|
|
|
@@ -467,10 +467,17 @@ p_openai_cfg = OpenAIProvider(
|
|
|
467
467
|
thinking=ThinkingConfig(effort="high", summary="auto"),
|
|
468
468
|
)
|
|
469
469
|
|
|
470
|
-
# 2. Anthropic: Configure
|
|
471
|
-
|
|
470
|
+
# 2. Anthropic: Configure adaptive thinking with reasoning summary, or manual budget tokens
|
|
471
|
+
# Adaptive thinking with reasoning summary:
|
|
472
|
+
p_anthropic_adaptive = AnthropicProvider(
|
|
472
473
|
model="claude-3-7-sonnet-20250219",
|
|
473
|
-
thinking=ThinkingConfig(
|
|
474
|
+
thinking=ThinkingConfig(effort="high", summary="auto"),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Extended manual budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed):
|
|
478
|
+
p_anthropic_budget = AnthropicProvider(
|
|
479
|
+
model="claude-3-7-sonnet-20250219",
|
|
480
|
+
thinking=ThinkingConfig(budget_tokens=4096, summary="omitted"),
|
|
474
481
|
)
|
|
475
482
|
|
|
476
483
|
# 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
|
|
@@ -490,9 +497,9 @@ p_gemini_budget = GeminiProvider(
|
|
|
490
497
|
|
|
491
498
|
| Field | Type | Description & Provider Mapping |
|
|
492
499
|
| :--- | :--- | :--- |
|
|
493
|
-
| `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`effort`). If an unexpected effort level is passed, the provider API handles it directly. |
|
|
500
|
+
| `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`output_config.effort` with adaptive thinking). If an unexpected effort level is passed, the provider API handles it directly. |
|
|
494
501
|
| `budget_tokens` | `int \| None` | Maximum reasoning token budget where supported. Supported by Anthropic (`thinking={"type": "enabled", "budget_tokens": ...}`) and Gemini (`thinking_budget`). |
|
|
495
|
-
| `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported
|
|
502
|
+
| `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported across all three providers:<br>• **Anthropic**: Controls reasoning exposure via Claude's `thinking.display` parameter (`"summarized"` vs `"omitted"`). Values such as `"auto"`, `"summarized"`, `"concise"`, or `"detailed"` set `display: "summarized"`, populating `response.thinking` with a human-readable summary. Values like `"none"` or `"omitted"` set `display: "omitted"`, omitting thinking text to minimize time-to-first-token while preserving cryptographic signatures in `thought_blocks` for multi-turn replay.<br>• **OpenAI**: Configures `reasoning.summary`.<br>• **Gemini**: Configures `thinking_summaries` (`"auto"` or `"none"`). As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
|
|
496
503
|
|
|
497
504
|
> [!NOTE]
|
|
498
505
|
> **No Boolean or Standalone Integer Shorthands**: Shorthands like `thinking=False` or `thinking=4096` are intentionally not supported to avoid ambiguity. To run without thinking, simply omit the parameter or pass `thinking=None`. To configure a token budget, use `ThinkingConfig(budget_tokens=4096)`.
|
|
@@ -519,7 +526,7 @@ if response.usage and response.usage.reasoning_tokens:
|
|
|
519
526
|
|
|
520
527
|
#### Multi-Turn Tool Calling & Cryptographic Signatures
|
|
521
528
|
|
|
522
|
-
Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling.
|
|
529
|
+
Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling. Furthermore, Evidor normalizes and sanitizes thought blocks across providers (e.g. converting between Gemini's and Anthropic's signature representations and stripping non-standard fields) so history can be transferred or replayed seamlessly without schema violations.
|
|
523
530
|
|
|
524
531
|
## Tools & Function Calling
|
|
525
532
|
|
|
@@ -4,8 +4,8 @@ from collections.abc import Sequence
|
|
|
4
4
|
import json
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from evidor.models import GenerationRequest, Message
|
|
8
|
-
from ..base import extract_system_prompt, sort_tools
|
|
7
|
+
from evidor.models import GenerationRequest, Message, ThinkingConfig
|
|
8
|
+
from ..base import extract_system_prompt, safe_parse_tool_arguments, sort_tools
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class AnthropicPayloadBuilder:
|
|
@@ -54,26 +54,61 @@ class AnthropicPayloadBuilder:
|
|
|
54
54
|
payload["output_config"] = {"effort": active_thinking}
|
|
55
55
|
elif active_thinking and not isinstance(active_thinking, (bool, int)):
|
|
56
56
|
budget = (
|
|
57
|
-
active_thinking.get("budget_tokens")
|
|
57
|
+
active_thinking.get("budget_tokens") or active_thinking.get("thinking_budget")
|
|
58
58
|
if isinstance(active_thinking, dict)
|
|
59
|
-
else getattr(active_thinking, "budget_tokens", None)
|
|
59
|
+
else getattr(active_thinking, "budget_tokens", None) or getattr(active_thinking, "thinking_budget", None)
|
|
60
60
|
)
|
|
61
61
|
effort = (
|
|
62
|
-
active_thinking.get("effort")
|
|
62
|
+
active_thinking.get("effort") or active_thinking.get("thinking_level")
|
|
63
63
|
if isinstance(active_thinking, dict)
|
|
64
|
-
else getattr(active_thinking, "effort", None)
|
|
64
|
+
else getattr(active_thinking, "effort", None) or getattr(active_thinking, "thinking_level", None)
|
|
65
65
|
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
raw_summary = (
|
|
67
|
+
active_thinking.get("summary") or active_thinking.get("display") or active_thinking.get("thinking_summaries")
|
|
68
|
+
if isinstance(active_thinking, dict)
|
|
69
|
+
else getattr(active_thinking, "summary", None) or getattr(active_thinking, "display", None) or getattr(active_thinking, "thinking_summaries", None)
|
|
70
|
+
)
|
|
71
|
+
explicit_type = (
|
|
72
|
+
active_thinking.get("type")
|
|
73
|
+
if isinstance(active_thinking, dict)
|
|
74
|
+
else getattr(active_thinking, "type", None)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
display_val: str | None = None
|
|
78
|
+
if isinstance(raw_summary, bool):
|
|
79
|
+
display_val = "summarized" if raw_summary else "omitted"
|
|
80
|
+
elif isinstance(raw_summary, str) and raw_summary.strip():
|
|
81
|
+
cleaned_summary = raw_summary.strip().lower()
|
|
82
|
+
if cleaned_summary in ("none", "omitted", "off", "hidden", "false"):
|
|
83
|
+
display_val = "omitted"
|
|
84
|
+
elif cleaned_summary in ("summarized", "summary", "auto", "concise", "detailed", "on", "visible", "true"):
|
|
85
|
+
display_val = "summarized"
|
|
86
|
+
else:
|
|
87
|
+
display_val = raw_summary.strip()
|
|
88
|
+
|
|
89
|
+
if (budget is not None and not isinstance(budget, bool) and budget > 0) or explicit_type == "enabled":
|
|
90
|
+
actual_budget = max(budget or 1024, 1024)
|
|
91
|
+
thinking_dict: dict[str, Any] = {
|
|
69
92
|
"type": "enabled",
|
|
70
93
|
"budget_tokens": actual_budget,
|
|
71
94
|
}
|
|
95
|
+
if display_val:
|
|
96
|
+
thinking_dict["display"] = display_val
|
|
97
|
+
payload["thinking"] = thinking_dict
|
|
72
98
|
if payload["max_tokens"] <= actual_budget:
|
|
73
99
|
payload["max_tokens"] = actual_budget + 1024
|
|
74
|
-
elif
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
elif (
|
|
101
|
+
isinstance(effort, str)
|
|
102
|
+
or display_val is not None
|
|
103
|
+
or explicit_type == "adaptive"
|
|
104
|
+
or isinstance(active_thinking, ThinkingConfig)
|
|
105
|
+
):
|
|
106
|
+
thinking_dict = {"type": "adaptive"}
|
|
107
|
+
if display_val:
|
|
108
|
+
thinking_dict["display"] = display_val
|
|
109
|
+
payload["thinking"] = thinking_dict
|
|
110
|
+
if isinstance(effort, str) and effort.strip():
|
|
111
|
+
payload["output_config"] = {"effort": effort.strip()}
|
|
77
112
|
|
|
78
113
|
if self.enable_prompt_caching:
|
|
79
114
|
payload["cache_control"] = {"type": "ephemeral"}
|
|
@@ -109,25 +144,65 @@ class AnthropicPayloadBuilder:
|
|
|
109
144
|
@staticmethod
|
|
110
145
|
def _normalize_thought_block(tb: Any) -> dict[str, Any] | None:
|
|
111
146
|
"""Convert a thought block (dict, Pydantic model, SDK object) to an Anthropic block dict."""
|
|
147
|
+
if tb is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
block_dict: dict[str, Any] | None = None
|
|
112
151
|
if isinstance(tb, dict):
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
block_dict = tb
|
|
153
|
+
elif hasattr(tb, "model_dump") and callable(tb.model_dump):
|
|
115
154
|
dumped = tb.model_dump()
|
|
116
155
|
if isinstance(dumped, dict):
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
block_dict = dumped
|
|
157
|
+
elif hasattr(tb, "to_dict") and callable(tb.to_dict):
|
|
119
158
|
dumped = tb.to_dict()
|
|
120
159
|
if isinstance(dumped, dict):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
block_dict = dumped
|
|
161
|
+
|
|
162
|
+
def _get(key: str, default: Any = None) -> Any:
|
|
163
|
+
if block_dict is not None:
|
|
164
|
+
return block_dict.get(key, default)
|
|
165
|
+
return getattr(tb, key, default)
|
|
166
|
+
|
|
167
|
+
raw_type = _get("type")
|
|
168
|
+
block_type = str(raw_type).strip().lower() if raw_type is not None else None
|
|
169
|
+
|
|
170
|
+
if block_type == "redacted_thinking":
|
|
171
|
+
data_val = _get("data", "")
|
|
172
|
+
return {
|
|
173
|
+
"type": "redacted_thinking",
|
|
174
|
+
"data": str(data_val or ""),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
is_thought = (
|
|
178
|
+
block_type in ("thinking", "thought", "reasoning")
|
|
179
|
+
or _get("thinking") is not None
|
|
180
|
+
or _get("thought") is not None
|
|
181
|
+
or _get("thought_signature") is not None
|
|
182
|
+
or _get("thoughtSignature") is not None
|
|
183
|
+
or _get("signature") is not None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if is_thought:
|
|
187
|
+
thinking_text = (
|
|
188
|
+
_get("thinking")
|
|
189
|
+
or _get("summary")
|
|
190
|
+
or _get("text")
|
|
191
|
+
or ""
|
|
192
|
+
)
|
|
193
|
+
sig = (
|
|
194
|
+
_get("signature")
|
|
195
|
+
or _get("thought_signature")
|
|
196
|
+
or _get("thoughtSignature")
|
|
197
|
+
)
|
|
198
|
+
normalized: dict[str, Any] = {
|
|
199
|
+
"type": "thinking",
|
|
200
|
+
"thinking": str(thinking_text) if thinking_text is not None else "",
|
|
201
|
+
}
|
|
202
|
+
if sig is not None and str(sig).strip():
|
|
203
|
+
normalized["signature"] = str(sig).strip()
|
|
204
|
+
return normalized
|
|
205
|
+
|
|
131
206
|
return None
|
|
132
207
|
|
|
133
208
|
def format_messages(self, messages: Sequence[Message]) -> list[dict[str, Any]]:
|
|
@@ -184,15 +259,27 @@ class AnthropicPayloadBuilder:
|
|
|
184
259
|
content_blocks.append({"type": "text", "text": message.content})
|
|
185
260
|
if message.tool_calls:
|
|
186
261
|
for tc in message.tool_calls:
|
|
262
|
+
tc_args = (
|
|
263
|
+
tc.arguments
|
|
264
|
+
if isinstance(tc.arguments, dict)
|
|
265
|
+
else (safe_parse_tool_arguments(tc.arguments) if tc.arguments else {})
|
|
266
|
+
)
|
|
187
267
|
content_blocks.append({
|
|
188
268
|
"type": "tool_use",
|
|
189
269
|
"id": tc.id,
|
|
190
270
|
"name": tc.name,
|
|
191
|
-
"input":
|
|
271
|
+
"input": tc_args,
|
|
192
272
|
})
|
|
273
|
+
if not content_blocks:
|
|
274
|
+
fallback_text = message.content if (isinstance(message.content, str) and message.content.strip()) else "Done."
|
|
275
|
+
content_blocks.append({"type": "text", "text": fallback_text})
|
|
193
276
|
formatted.append({"role": "assistant", "content": content_blocks})
|
|
194
277
|
else:
|
|
195
|
-
|
|
278
|
+
if isinstance(message.content, list):
|
|
279
|
+
formatted.append({"role": "assistant", "content": message.content})
|
|
280
|
+
else:
|
|
281
|
+
content_val = message.content if (isinstance(message.content, str) and message.content.strip()) else "Done."
|
|
282
|
+
formatted.append({"role": "assistant", "content": content_val})
|
|
196
283
|
|
|
197
284
|
else:
|
|
198
285
|
# User or other role
|
|
@@ -39,9 +39,18 @@ class AnthropicResponseParser:
|
|
|
39
39
|
|
|
40
40
|
thinking_text = "".join(thought_parts) if thought_parts else None
|
|
41
41
|
if thinking_text is None:
|
|
42
|
-
raw_thinking =
|
|
42
|
+
raw_thinking = (
|
|
43
|
+
_get_field(response_msg, "thinking")
|
|
44
|
+
or _get_field(response_msg, "reasoning")
|
|
45
|
+
or _get_field(response_msg, "thinking_summary")
|
|
46
|
+
or _get_field(response_msg, "summary")
|
|
47
|
+
)
|
|
43
48
|
if isinstance(raw_thinking, str) and raw_thinking.strip():
|
|
44
49
|
thinking_text = raw_thinking.strip()
|
|
50
|
+
elif isinstance(raw_thinking, dict):
|
|
51
|
+
th = raw_thinking.get("thinking") or raw_thinking.get("summary") or raw_thinking.get("text")
|
|
52
|
+
if isinstance(th, str) and th.strip():
|
|
53
|
+
thinking_text = th.strip()
|
|
45
54
|
|
|
46
55
|
usage = self.parse_usage(_get_field(response_msg, "usage"))
|
|
47
56
|
|
|
@@ -93,10 +102,20 @@ class AnthropicResponseParser:
|
|
|
93
102
|
)
|
|
94
103
|
if c:
|
|
95
104
|
compaction_summary = str(c)
|
|
96
|
-
elif block_type
|
|
97
|
-
th_text =
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
elif block_type in ("thinking", "thought"):
|
|
106
|
+
th_text = (
|
|
107
|
+
_get_field(block, "thinking")
|
|
108
|
+
or _get_field(block, "summary")
|
|
109
|
+
or _get_field(block, "text")
|
|
110
|
+
or ""
|
|
111
|
+
)
|
|
112
|
+
if str(th_text).strip():
|
|
113
|
+
thought_parts.append(str(th_text))
|
|
114
|
+
sig = (
|
|
115
|
+
_get_field(block, "signature")
|
|
116
|
+
or _get_field(block, "thought_signature")
|
|
117
|
+
or _get_field(block, "thoughtSignature")
|
|
118
|
+
)
|
|
100
119
|
tb: dict[str, Any] = {"type": "thinking", "thinking": str(th_text)}
|
|
101
120
|
if isinstance(sig, str) and sig.strip():
|
|
102
121
|
tb["signature"] = sig.strip()
|
|
@@ -53,8 +53,8 @@ class AnthropicStreamHandler:
|
|
|
53
53
|
name = _get_field(cb, "name") or "unknown_tool"
|
|
54
54
|
self.block_id_map[idx] = str(call_id)
|
|
55
55
|
self.tool_acc.start_call(str(call_id), str(name))
|
|
56
|
-
elif cb_type
|
|
57
|
-
th_text = _get_field(cb, "thinking") or ""
|
|
56
|
+
elif cb_type in ("thinking", "thought"):
|
|
57
|
+
th_text = _get_field(cb, "thinking") or _get_field(cb, "summary") or ""
|
|
58
58
|
self.block_thinking_map[idx] = {
|
|
59
59
|
"type": "thinking",
|
|
60
60
|
"parts": [str(th_text)] if th_text else [],
|
|
@@ -85,8 +85,17 @@ class AnthropicStreamHandler:
|
|
|
85
85
|
if txt:
|
|
86
86
|
self.accumulated_text.append(str(txt))
|
|
87
87
|
yield StreamChunk(text=str(txt), raw_event=event)
|
|
88
|
-
elif
|
|
89
|
-
|
|
88
|
+
elif (
|
|
89
|
+
delta_type in (
|
|
90
|
+
"thinking_delta",
|
|
91
|
+
"thinking_summary_delta",
|
|
92
|
+
"reasoning_delta",
|
|
93
|
+
"reasoning_summary_delta",
|
|
94
|
+
)
|
|
95
|
+
or hasattr(delta, "thinking")
|
|
96
|
+
or hasattr(delta, "summary")
|
|
97
|
+
):
|
|
98
|
+
th = _get_field(delta, "thinking", "") or _get_field(delta, "summary", "") or ""
|
|
90
99
|
if th:
|
|
91
100
|
self.text_thinking_parts.append(str(th))
|
|
92
101
|
if idx in self.block_thinking_map and "parts" in self.block_thinking_map[idx]:
|
|
@@ -101,7 +110,9 @@ class AnthropicStreamHandler:
|
|
|
101
110
|
elif delta_type == "signature_delta" or hasattr(delta, "signature"):
|
|
102
111
|
sig = _get_field(delta, "signature", "") or ""
|
|
103
112
|
if sig and idx in self.block_thinking_map:
|
|
104
|
-
self.block_thinking_map[idx]["signature"] =
|
|
113
|
+
self.block_thinking_map[idx]["signature"] = (
|
|
114
|
+
self.block_thinking_map[idx].get("signature", "") + str(sig)
|
|
115
|
+
)
|
|
105
116
|
elif delta_type == "input_json_delta" or hasattr(delta, "partial_json"):
|
|
106
117
|
call_id = self.block_id_map.get(idx, f"call_{idx}")
|
|
107
118
|
pj = _get_field(delta, "partial_json", "") or ""
|
|
@@ -152,11 +152,11 @@ class GeminiResponseParser:
|
|
|
152
152
|
return val
|
|
153
153
|
return default
|
|
154
154
|
|
|
155
|
-
pt = _get_field("total_input_tokens"
|
|
156
|
-
ct = _get_field("total_output_tokens"
|
|
155
|
+
pt = _get_field("total_input_tokens")
|
|
156
|
+
ct = _get_field("total_output_tokens")
|
|
157
157
|
tt = _get_field("total_tokens")
|
|
158
|
-
cached = _get_field("total_cached_tokens",
|
|
159
|
-
reasoning = _get_field("total_thought_tokens",
|
|
158
|
+
cached = _get_field("total_cached_tokens", default=0) or 0
|
|
159
|
+
reasoning = _get_field("total_thought_tokens", default=0) or 0
|
|
160
160
|
|
|
161
161
|
if not (isinstance(pt, int) or isinstance(ct, int) or isinstance(tt, int)):
|
|
162
162
|
return None
|
|
@@ -224,6 +224,199 @@ def test_anthropic_provider_multiturn_thought_signature_replay(monkeypatch: pyte
|
|
|
224
224
|
assert content_blocks[1]["id"] == "call_99"
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
def test_anthropic_builder_adaptive_effort_and_summary() -> None:
|
|
228
|
+
from evidor.agent.providers.anthropic.builder import AnthropicPayloadBuilder
|
|
229
|
+
|
|
230
|
+
# 1. Adaptive with effort and summary="auto" -> display: "summarized"
|
|
231
|
+
cfg1 = ThinkingConfig(effort="high", summary="auto")
|
|
232
|
+
b1 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg1)
|
|
233
|
+
p1 = b1.build(GenerationRequest(prompt="Explain quantum mechanics"))
|
|
234
|
+
assert p1["thinking"] == {"type": "adaptive", "display": "summarized"}
|
|
235
|
+
assert p1["output_config"] == {"effort": "high"}
|
|
236
|
+
|
|
237
|
+
# 2. Adaptive with effort and summary="none" -> display: "omitted"
|
|
238
|
+
cfg2 = ThinkingConfig(effort="low", summary="none")
|
|
239
|
+
b2 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg2)
|
|
240
|
+
p2 = b2.build(GenerationRequest(prompt="Simple math"))
|
|
241
|
+
assert p2["thinking"] == {"type": "adaptive", "display": "omitted"}
|
|
242
|
+
assert p2["output_config"] == {"effort": "low"}
|
|
243
|
+
|
|
244
|
+
# 3. Summary only ("detailed") -> adaptive thinking with display: "summarized"
|
|
245
|
+
cfg3 = ThinkingConfig(summary="detailed")
|
|
246
|
+
b3 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg3)
|
|
247
|
+
p3 = b3.build(GenerationRequest(prompt="Summarize"))
|
|
248
|
+
assert p3["thinking"] == {"type": "adaptive", "display": "summarized"}
|
|
249
|
+
assert "output_config" not in p3
|
|
250
|
+
|
|
251
|
+
# 4. Summary only ("omitted") -> adaptive thinking with display: "omitted"
|
|
252
|
+
cfg4 = ThinkingConfig(summary="omitted")
|
|
253
|
+
b4 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg4)
|
|
254
|
+
p4 = b4.build(GenerationRequest(prompt="Code quickly"))
|
|
255
|
+
assert p4["thinking"] == {"type": "adaptive", "display": "omitted"}
|
|
256
|
+
|
|
257
|
+
# 5. Budget tokens with summary="concise" -> display: "summarized"
|
|
258
|
+
cfg5 = ThinkingConfig(budget_tokens=2048, summary="concise")
|
|
259
|
+
b5 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg5)
|
|
260
|
+
p5 = b5.build(GenerationRequest(prompt="Analyze code"))
|
|
261
|
+
assert p5["thinking"] == {"type": "enabled", "budget_tokens": 2048, "display": "summarized"}
|
|
262
|
+
assert p5["max_tokens"] > 2048
|
|
263
|
+
|
|
264
|
+
# 6. Budget tokens with summary="none" -> display: "omitted"
|
|
265
|
+
cfg6 = ThinkingConfig(budget_tokens=2048, summary="none")
|
|
266
|
+
b6 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=4096, thinking=cfg6)
|
|
267
|
+
p6 = b6.build(GenerationRequest(prompt="Deep benchmark"))
|
|
268
|
+
assert p6["thinking"] == {"type": "enabled", "budget_tokens": 2048, "display": "omitted"}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_anthropic_builder_thought_block_normalization_cross_provider() -> None:
|
|
272
|
+
from evidor.agent.providers.anthropic.builder import AnthropicPayloadBuilder
|
|
273
|
+
|
|
274
|
+
builder = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048)
|
|
275
|
+
|
|
276
|
+
# Gemini-style thought block with thought_signature and boolean thought flag
|
|
277
|
+
gem_block = {
|
|
278
|
+
"type": "thought",
|
|
279
|
+
"text": "Gemini chain-of-thought",
|
|
280
|
+
"thought": True,
|
|
281
|
+
"thought_signature": "gem_sig_789",
|
|
282
|
+
"extra_gemini_field": "ignore_me",
|
|
283
|
+
}
|
|
284
|
+
norm_gem = builder._normalize_thought_block(gem_block)
|
|
285
|
+
assert norm_gem == {
|
|
286
|
+
"type": "thinking",
|
|
287
|
+
"thinking": "Gemini chain-of-thought",
|
|
288
|
+
"signature": "gem_sig_789",
|
|
289
|
+
}
|
|
290
|
+
assert "extra_gemini_field" not in norm_gem
|
|
291
|
+
assert "thought" not in norm_gem
|
|
292
|
+
|
|
293
|
+
# OpenAI-style reasoning block with summary
|
|
294
|
+
oai_block = {
|
|
295
|
+
"type": "reasoning",
|
|
296
|
+
"summary": "OpenAI reasoning summary",
|
|
297
|
+
"signature": "oai_sig_321",
|
|
298
|
+
}
|
|
299
|
+
norm_oai = builder._normalize_thought_block(oai_block)
|
|
300
|
+
assert norm_oai == {
|
|
301
|
+
"type": "thinking",
|
|
302
|
+
"thinking": "OpenAI reasoning summary",
|
|
303
|
+
"signature": "oai_sig_321",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Anthropic omitted block (empty thinking)
|
|
307
|
+
omitted_block = {
|
|
308
|
+
"type": "thinking",
|
|
309
|
+
"thinking": "",
|
|
310
|
+
"signature": "omitted_sig_999",
|
|
311
|
+
}
|
|
312
|
+
norm_omit = builder._normalize_thought_block(omitted_block)
|
|
313
|
+
assert norm_omit == {
|
|
314
|
+
"type": "thinking",
|
|
315
|
+
"thinking": "",
|
|
316
|
+
"signature": "omitted_sig_999",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Redacted thinking block
|
|
320
|
+
redacted_block = {
|
|
321
|
+
"type": "redacted_thinking",
|
|
322
|
+
"data": "encrypted_thought_data",
|
|
323
|
+
}
|
|
324
|
+
norm_redacted = builder._normalize_thought_block(redacted_block)
|
|
325
|
+
assert norm_redacted == {
|
|
326
|
+
"type": "redacted_thinking",
|
|
327
|
+
"data": "encrypted_thought_data",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Replay assistant message with normalized blocks and fallback for empty content
|
|
331
|
+
req = GenerationRequest(
|
|
332
|
+
messages=[
|
|
333
|
+
Message(role="user", content="Test"),
|
|
334
|
+
Message(role="assistant", content="", thought_blocks=(gem_block,)),
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
payload = builder.build(req)
|
|
338
|
+
asst_content = payload["messages"][1]["content"]
|
|
339
|
+
assert len(asst_content) == 1
|
|
340
|
+
assert asst_content[0]["type"] == "thinking"
|
|
341
|
+
assert asst_content[0]["signature"] == "gem_sig_789"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_anthropic_parser_reasoning_summary_and_omitted() -> None:
|
|
345
|
+
from evidor.agent.providers.anthropic.parser import AnthropicResponseParser
|
|
346
|
+
|
|
347
|
+
parser = AnthropicResponseParser(model="claude-3-7-sonnet")
|
|
348
|
+
|
|
349
|
+
# 1. Summarized thinking block: thinking has text
|
|
350
|
+
resp_sum = {
|
|
351
|
+
"id": "msg_sum_01",
|
|
352
|
+
"content": [
|
|
353
|
+
{"type": "thinking", "summary": "Core algorithm design steps", "signature": "sig_sum_1"},
|
|
354
|
+
{"type": "text", "text": "Here is the implementation"},
|
|
355
|
+
],
|
|
356
|
+
}
|
|
357
|
+
res_sum = parser.parse(resp_sum)
|
|
358
|
+
assert res_sum.thinking == "Core algorithm design steps"
|
|
359
|
+
assert len(res_sum.thought_blocks) == 1
|
|
360
|
+
assert res_sum.thought_blocks[0] == {
|
|
361
|
+
"type": "thinking",
|
|
362
|
+
"thinking": "Core algorithm design steps",
|
|
363
|
+
"signature": "sig_sum_1",
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# 2. Omitted thinking block: thinking is empty string ""
|
|
367
|
+
resp_omit = {
|
|
368
|
+
"id": "msg_omit_02",
|
|
369
|
+
"content": [
|
|
370
|
+
{"type": "thinking", "thinking": "", "signature": "sig_omit_2"},
|
|
371
|
+
{"type": "text", "text": "Fast response"},
|
|
372
|
+
],
|
|
373
|
+
}
|
|
374
|
+
res_omit = parser.parse(resp_omit)
|
|
375
|
+
assert res_omit.thinking is None # Omitted thinking text should not expose empty string
|
|
376
|
+
assert len(res_omit.thought_blocks) == 1
|
|
377
|
+
assert res_omit.thought_blocks[0] == {
|
|
378
|
+
"type": "thinking",
|
|
379
|
+
"thinking": "",
|
|
380
|
+
"signature": "sig_omit_2",
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_anthropic_stream_reasoning_summary_and_signature_accumulation() -> None:
|
|
385
|
+
from evidor.agent.providers.anthropic.stream import AnthropicStreamHandler
|
|
386
|
+
|
|
387
|
+
handler = AnthropicStreamHandler(model="claude-3-7-sonnet")
|
|
388
|
+
|
|
389
|
+
events = [
|
|
390
|
+
{"type": "message_start", "message": {"id": "msg_str_01", "model": "claude-3-7-sonnet"}},
|
|
391
|
+
{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}},
|
|
392
|
+
{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "First reasoning step. "}},
|
|
393
|
+
{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_summary_delta", "summary": "Second reasoning step."}},
|
|
394
|
+
{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig_part1_"}},
|
|
395
|
+
{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig_part2"}},
|
|
396
|
+
{"type": "content_block_stop", "index": 0},
|
|
397
|
+
{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}},
|
|
398
|
+
{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Final answer."}},
|
|
399
|
+
{"type": "content_block_stop", "index": 1},
|
|
400
|
+
{"type": "message_stop"},
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
chunks = []
|
|
404
|
+
for ev in events:
|
|
405
|
+
chunks.extend(list(handler.process_event(ev)))
|
|
406
|
+
|
|
407
|
+
finished_chunk = next(c for c in chunks if c.is_finished)
|
|
408
|
+
final_resp = finished_chunk.response
|
|
409
|
+
assert final_resp is not None
|
|
410
|
+
assert final_resp.text == "Final answer."
|
|
411
|
+
assert final_resp.thinking == "First reasoning step. Second reasoning step."
|
|
412
|
+
assert len(final_resp.thought_blocks) == 1
|
|
413
|
+
assert final_resp.thought_blocks[0] == {
|
|
414
|
+
"type": "thinking",
|
|
415
|
+
"thinking": "First reasoning step. Second reasoning step.",
|
|
416
|
+
"signature": "sig_part1_sig_part2",
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
227
420
|
# ==============================================================================
|
|
228
421
|
# GeminiProvider Thinking Tests
|
|
229
422
|
# ==============================================================================
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/mutate_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/search_files.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/__init__.py
RENAMED
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/brave.py
RENAMED
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/exa.py
RENAMED
|
File without changes
|
{evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/tavily.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|