selectools 0.16.4__tar.gz → 0.16.6__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.
- {selectools-0.16.4/src/selectools.egg-info → selectools-0.16.6}/PKG-INFO +20 -16
- {selectools-0.16.4 → selectools-0.16.6}/README.md +19 -15
- {selectools-0.16.4 → selectools-0.16.6}/pyproject.toml +12 -2
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/__init__.py +17 -4
- selectools-0.16.6/src/selectools/agent/_lifecycle.py +141 -0
- selectools-0.16.6/src/selectools/agent/_memory_manager.py +146 -0
- selectools-0.16.6/src/selectools/agent/_provider_caller.py +459 -0
- selectools-0.16.6/src/selectools/agent/_tool_executor.py +954 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/agent/config.py +1 -0
- selectools-0.16.6/src/selectools/agent/core.py +1448 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/models.py +161 -148
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/observer.py +380 -1
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/__init__.py +5 -0
- selectools-0.16.4/src/selectools/providers/openai_provider.py → selectools-0.16.6/src/selectools/providers/_openai_compat.py +218 -221
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/gemini_provider.py +68 -10
- selectools-0.16.6/src/selectools/providers/ollama_provider.py +126 -0
- selectools-0.16.6/src/selectools/providers/openai_provider.py +86 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/base.py +4 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/decorators.py +4 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/trace.py +31 -22
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/types.py +8 -1
- {selectools-0.16.4 → selectools-0.16.6/src/selectools.egg-info}/PKG-INFO +20 -16
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/SOURCES.txt +9 -0
- selectools-0.16.6/tests/test_architecture.py +242 -0
- selectools-0.16.6/tests/test_async_observers.py +374 -0
- selectools-0.16.6/tests/test_phase1_design_patterns.py +694 -0
- selectools-0.16.6/tests/test_terminal_actions.py +410 -0
- selectools-0.16.4/src/selectools/agent/core.py +0 -3128
- selectools-0.16.4/src/selectools/providers/ollama_provider.py +0 -456
- {selectools-0.16.4 → selectools-0.16.6}/LICENSE +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/setup.cfg +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/agent/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/analytics.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/audit.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cache.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cache_redis.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cli.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/coherence.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/anthropic.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/cohere.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/gemini.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/openai.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/provider.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/entity_memory.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/env.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/exceptions.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/base.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/format.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/length.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/pii.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/pipeline.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/topic.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/toxicity.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/knowledge.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/knowledge_graph.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/memory.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/parser.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/policy.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/pricing.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/prompt.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/anthropic_provider.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/base.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/fallback.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/stubs.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/bm25.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/chunking.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/hybrid.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/loaders.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/reranker.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/chroma.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/memory.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/pinecone.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/sqlite.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/vector_store.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/security.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/sessions.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/structured.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/data_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/datetime_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/file_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/memory_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/text_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/web_tools.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/__init__.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/loader.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/registry.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools/usage.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/dependency_links.txt +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/entry_points.txt +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/requires.txt +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/top_level.txt +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_audit.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_cache.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_cache_redis.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_cli.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_coherence.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_consolidation_regression.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_entity_memory.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_env.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_guardrails.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_knowledge.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_knowledge_graph.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_async.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_boundary.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_integration.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_parser.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_policy.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_prompt.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_routing_mode.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_security.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions_edge_cases.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions_redis.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_structured.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_summarize_on_trim.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_trace.py +0 -0
- {selectools-0.16.4 → selectools-0.16.6}/tests/test_v016_regression.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: selectools
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.6
|
|
4
4
|
Summary: Production-ready AI agents with tool calling, structured output, execution traces, and RAG. Provider-agnostic (OpenAI, Anthropic, Gemini, Ollama) with fallback chains, batch processing, tool policies, streaming, caching, and cost tracking.
|
|
5
5
|
Author-email: John <johnnichev@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0-or-later
|
|
@@ -52,23 +52,27 @@ Dynamic: license-file
|
|
|
52
52
|
[](https://badge.fury.io/py/selectools)
|
|
53
53
|
[](https://johnnichev.github.io/selectools)
|
|
54
54
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
55
|
-
[](https://www.python.org/downloads/)
|
|
56
56
|
|
|
57
57
|
**Production-ready AI agents with tool calling, RAG, and hybrid search.** Connect LLMs to your Python functions, embed and search your documents with vector + keyword fusion, stream responses in real time, and dynamically manage tools at runtime. Works with OpenAI, Anthropic, Gemini, and Ollama. Tracks costs automatically.
|
|
58
58
|
|
|
59
|
-
## What's New in v0.16.
|
|
59
|
+
## What's New in v0.16.6
|
|
60
60
|
|
|
61
|
-
**
|
|
61
|
+
**Gemini 3.x thought_signature crash fix** — `thought_signature` from Gemini 3.x is opaque binary (protobuf/hash), not UTF-8 text. The previous code crashed with `UnicodeDecodeError` on non-UTF-8 bytes. Now uses base64 for lossless binary round-trip across all code paths.
|
|
62
62
|
|
|
63
|
-
- **
|
|
64
|
-
- **Summarize-on-Trim** — LLM-generated summaries of trimmed messages, injected as system context. No more silent context loss.
|
|
65
|
-
- **Entity Memory** — Auto-extract named entities (person, org, project) across turns with LRU-pruned registry and system prompt injection.
|
|
66
|
-
- **Knowledge Graph** — Relationship triple extraction with in-memory and SQLite storage. Query-relevant triples auto-injected into prompts.
|
|
67
|
-
- **Cross-Session Knowledge** — Daily logs + persistent facts with auto-registered `remember` tool. Give your agent durable memory across conversations.
|
|
68
|
-
- **182 new tests** (total: 1462 with v0.16.3 refactoring)
|
|
63
|
+
- **1642 tests** — 2 new regression tests for non-UTF-8 binary thought_signature round-trip
|
|
69
64
|
|
|
70
65
|
> Full changelog: [CHANGELOG.md](https://github.com/johnnichev/selectools/blob/main/CHANGELOG.md)
|
|
71
66
|
|
|
67
|
+
<details>
|
|
68
|
+
<summary><strong>v0.16.x highlights</strong></summary>
|
|
69
|
+
|
|
70
|
+
- **v0.16.5**: Design Patterns & Code Quality — terminal actions, async observers, Gemini 3.x thought signatures, agent decomposition, hooks deprecated
|
|
71
|
+
- **v0.16.4**: Parallel execution safety — coherence + screening in parallel, guardrail immutability, streaming usage tracking
|
|
72
|
+
- **v0.16.0**: Memory & Persistence — persistent sessions (3 backends), summarize-on-trim, entity memory, knowledge graph
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
72
76
|
<details>
|
|
73
77
|
<summary><strong>v0.15.x highlights</strong></summary>
|
|
74
78
|
|
|
@@ -80,7 +84,7 @@ Dynamic: license-file
|
|
|
80
84
|
<summary><strong>v0.14.x highlights</strong></summary>
|
|
81
85
|
|
|
82
86
|
- **v0.14.1**: Critical streaming fix — 13 bugs fixed across all providers; 141 new tests (total: 1100)
|
|
83
|
-
- **v0.14.0**: AgentObserver Protocol (
|
|
87
|
+
- **v0.14.0**: AgentObserver Protocol (25 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
|
|
84
88
|
|
|
85
89
|
</details>
|
|
86
90
|
|
|
@@ -109,7 +113,7 @@ Dynamic: license-file
|
|
|
109
113
|
| **Entity Memory** | LLM-based entity extraction with deduplication, LRU pruning, and system prompt injection. |
|
|
110
114
|
| **Knowledge Graph** | Relationship triple extraction with in-memory and SQLite storage and keyword-based querying. |
|
|
111
115
|
| **Cross-Session Knowledge** | Daily logs + persistent facts with auto-registered `remember` tool. |
|
|
112
|
-
| **AgentObserver Protocol** |
|
|
116
|
+
| **AgentObserver Protocol** | 25-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
|
|
113
117
|
| **Production Hardened** | Retries with backoff, per-tool timeouts, iteration caps, cost warnings, observability hooks + observers. |
|
|
114
118
|
| **Library-First** | Not a framework. No magic globals, no hidden state. Use as much or as little as you need. |
|
|
115
119
|
|
|
@@ -133,9 +137,9 @@ Dynamic: license-file
|
|
|
133
137
|
- **Entity Memory**: LLM-based named entity extraction and tracking
|
|
134
138
|
- **Knowledge Graph**: Triple extraction with in-memory and SQLite storage
|
|
135
139
|
- **Cross-Session Knowledge**: Daily logs + persistent memory with `remember` tool
|
|
136
|
-
- **
|
|
140
|
+
- **38 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
|
|
137
141
|
- **AgentObserver Protocol**: 25 lifecycle events with `run_id` correlation, `LoggingObserver`, OTel export
|
|
138
|
-
- **
|
|
142
|
+
- **1642 Tests**: Unit, integration, regression, and E2E with real API calls
|
|
139
143
|
|
|
140
144
|
## Install
|
|
141
145
|
|
|
@@ -457,7 +461,7 @@ agent = Agent(
|
|
|
457
461
|
)
|
|
458
462
|
```
|
|
459
463
|
|
|
460
|
-
|
|
464
|
+
25 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim, guardrail, coherence, screening, session, entity, KG. See `observer.py` for full reference.
|
|
461
465
|
|
|
462
466
|
### E2E Streaming & Parallel Execution
|
|
463
467
|
|
|
@@ -627,7 +631,7 @@ Examples are numbered by difficulty. Start from 01 and work your way up.
|
|
|
627
631
|
|---|---|---|---|
|
|
628
632
|
| 01 | `01_hello_world.py` | First agent, `@tool`, `ask()` | No |
|
|
629
633
|
| 02 | `02_search_weather.py` | ToolRegistry, multiple tools | No |
|
|
630
|
-
| 03 | `03_toolbox.py` |
|
|
634
|
+
| 03 | `03_toolbox.py` | 24 pre-built tools (file, data, text, datetime, web) | No |
|
|
631
635
|
| 04 | `04_conversation_memory.py` | Multi-turn memory | Yes |
|
|
632
636
|
| 05 | `05_cost_tracking.py` | Token counting, cost warnings | Yes |
|
|
633
637
|
| 06 | `06_async_agent.py` | `arun()`, concurrent agents, FastAPI | Yes |
|
|
@@ -3,23 +3,27 @@
|
|
|
3
3
|
[](https://badge.fury.io/py/selectools)
|
|
4
4
|
[](https://johnnichev.github.io/selectools)
|
|
5
5
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
6
|
-
[](https://www.python.org/downloads/)
|
|
7
7
|
|
|
8
8
|
**Production-ready AI agents with tool calling, RAG, and hybrid search.** Connect LLMs to your Python functions, embed and search your documents with vector + keyword fusion, stream responses in real time, and dynamically manage tools at runtime. Works with OpenAI, Anthropic, Gemini, and Ollama. Tracks costs automatically.
|
|
9
9
|
|
|
10
|
-
## What's New in v0.16.
|
|
10
|
+
## What's New in v0.16.6
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Gemini 3.x thought_signature crash fix** — `thought_signature` from Gemini 3.x is opaque binary (protobuf/hash), not UTF-8 text. The previous code crashed with `UnicodeDecodeError` on non-UTF-8 bytes. Now uses base64 for lossless binary round-trip across all code paths.
|
|
13
13
|
|
|
14
|
-
- **
|
|
15
|
-
- **Summarize-on-Trim** — LLM-generated summaries of trimmed messages, injected as system context. No more silent context loss.
|
|
16
|
-
- **Entity Memory** — Auto-extract named entities (person, org, project) across turns with LRU-pruned registry and system prompt injection.
|
|
17
|
-
- **Knowledge Graph** — Relationship triple extraction with in-memory and SQLite storage. Query-relevant triples auto-injected into prompts.
|
|
18
|
-
- **Cross-Session Knowledge** — Daily logs + persistent facts with auto-registered `remember` tool. Give your agent durable memory across conversations.
|
|
19
|
-
- **182 new tests** (total: 1462 with v0.16.3 refactoring)
|
|
14
|
+
- **1642 tests** — 2 new regression tests for non-UTF-8 binary thought_signature round-trip
|
|
20
15
|
|
|
21
16
|
> Full changelog: [CHANGELOG.md](https://github.com/johnnichev/selectools/blob/main/CHANGELOG.md)
|
|
22
17
|
|
|
18
|
+
<details>
|
|
19
|
+
<summary><strong>v0.16.x highlights</strong></summary>
|
|
20
|
+
|
|
21
|
+
- **v0.16.5**: Design Patterns & Code Quality — terminal actions, async observers, Gemini 3.x thought signatures, agent decomposition, hooks deprecated
|
|
22
|
+
- **v0.16.4**: Parallel execution safety — coherence + screening in parallel, guardrail immutability, streaming usage tracking
|
|
23
|
+
- **v0.16.0**: Memory & Persistence — persistent sessions (3 backends), summarize-on-trim, entity memory, knowledge graph
|
|
24
|
+
|
|
25
|
+
</details>
|
|
26
|
+
|
|
23
27
|
<details>
|
|
24
28
|
<summary><strong>v0.15.x highlights</strong></summary>
|
|
25
29
|
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
<summary><strong>v0.14.x highlights</strong></summary>
|
|
32
36
|
|
|
33
37
|
- **v0.14.1**: Critical streaming fix — 13 bugs fixed across all providers; 141 new tests (total: 1100)
|
|
34
|
-
- **v0.14.0**: AgentObserver Protocol (
|
|
38
|
+
- **v0.14.0**: AgentObserver Protocol (25 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
|
|
35
39
|
|
|
36
40
|
</details>
|
|
37
41
|
|
|
@@ -60,7 +64,7 @@
|
|
|
60
64
|
| **Entity Memory** | LLM-based entity extraction with deduplication, LRU pruning, and system prompt injection. |
|
|
61
65
|
| **Knowledge Graph** | Relationship triple extraction with in-memory and SQLite storage and keyword-based querying. |
|
|
62
66
|
| **Cross-Session Knowledge** | Daily logs + persistent facts with auto-registered `remember` tool. |
|
|
63
|
-
| **AgentObserver Protocol** |
|
|
67
|
+
| **AgentObserver Protocol** | 25-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
|
|
64
68
|
| **Production Hardened** | Retries with backoff, per-tool timeouts, iteration caps, cost warnings, observability hooks + observers. |
|
|
65
69
|
| **Library-First** | Not a framework. No magic globals, no hidden state. Use as much or as little as you need. |
|
|
66
70
|
|
|
@@ -84,9 +88,9 @@
|
|
|
84
88
|
- **Entity Memory**: LLM-based named entity extraction and tracking
|
|
85
89
|
- **Knowledge Graph**: Triple extraction with in-memory and SQLite storage
|
|
86
90
|
- **Cross-Session Knowledge**: Daily logs + persistent memory with `remember` tool
|
|
87
|
-
- **
|
|
91
|
+
- **38 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
|
|
88
92
|
- **AgentObserver Protocol**: 25 lifecycle events with `run_id` correlation, `LoggingObserver`, OTel export
|
|
89
|
-
- **
|
|
93
|
+
- **1642 Tests**: Unit, integration, regression, and E2E with real API calls
|
|
90
94
|
|
|
91
95
|
## Install
|
|
92
96
|
|
|
@@ -408,7 +412,7 @@ agent = Agent(
|
|
|
408
412
|
)
|
|
409
413
|
```
|
|
410
414
|
|
|
411
|
-
|
|
415
|
+
25 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim, guardrail, coherence, screening, session, entity, KG. See `observer.py` for full reference.
|
|
412
416
|
|
|
413
417
|
### E2E Streaming & Parallel Execution
|
|
414
418
|
|
|
@@ -578,7 +582,7 @@ Examples are numbered by difficulty. Start from 01 and work your way up.
|
|
|
578
582
|
|---|---|---|---|
|
|
579
583
|
| 01 | `01_hello_world.py` | First agent, `@tool`, `ask()` | No |
|
|
580
584
|
| 02 | `02_search_weather.py` | ToolRegistry, multiple tools | No |
|
|
581
|
-
| 03 | `03_toolbox.py` |
|
|
585
|
+
| 03 | `03_toolbox.py` | 24 pre-built tools (file, data, text, datetime, web) | No |
|
|
582
586
|
| 04 | `04_conversation_memory.py` | Multi-turn memory | Yes |
|
|
583
587
|
| 05 | `05_cost_tracking.py` | Token counting, cost warnings | Yes |
|
|
584
588
|
| 06 | `06_async_agent.py` | `arun()`, concurrent agents, FastAPI | Yes |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "selectools"
|
|
7
|
-
version = "0.16.
|
|
7
|
+
version = "0.16.6"
|
|
8
8
|
description = "Production-ready AI agents with tool calling, structured output, execution traces, and RAG. Provider-agnostic (OpenAI, Anthropic, Gemini, Ollama) with fallback chains, batch processing, tool policies, streaming, caching, and cost tracking."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -96,7 +96,7 @@ use_parentheses = true
|
|
|
96
96
|
ensure_newline_before_comments = true
|
|
97
97
|
|
|
98
98
|
[tool.mypy]
|
|
99
|
-
python_version = "3.
|
|
99
|
+
python_version = "3.13"
|
|
100
100
|
warn_return_any = true
|
|
101
101
|
warn_unused_configs = true
|
|
102
102
|
disallow_untyped_defs = true
|
|
@@ -108,6 +108,16 @@ exclude = [
|
|
|
108
108
|
"dist/",
|
|
109
109
|
]
|
|
110
110
|
|
|
111
|
+
[[tool.mypy.overrides]]
|
|
112
|
+
module = [
|
|
113
|
+
"selectools.agent._tool_executor",
|
|
114
|
+
"selectools.agent._provider_caller",
|
|
115
|
+
"selectools.agent._lifecycle",
|
|
116
|
+
"selectools.agent._memory_manager",
|
|
117
|
+
"selectools.providers._openai_compat",
|
|
118
|
+
]
|
|
119
|
+
disable_error_code = ["attr-defined"]
|
|
120
|
+
|
|
111
121
|
[tool.bandit]
|
|
112
122
|
exclude_dirs = ["tests", "examples", "scripts"]
|
|
113
123
|
skips = ["B101", "B601", "B602"] # Skip assert_used, shell, and subprocess checks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Public exports for the selectools package."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.16.
|
|
3
|
+
__version__ = "0.16.6"
|
|
4
4
|
|
|
5
5
|
# Import submodules (lazy loading for optional dependencies)
|
|
6
6
|
from . import embeddings, guardrails, models, rag, toolbox
|
|
@@ -39,8 +39,18 @@ from .knowledge_graph import (
|
|
|
39
39
|
TripleStore,
|
|
40
40
|
)
|
|
41
41
|
from .memory import ConversationMemory
|
|
42
|
-
from .models import
|
|
43
|
-
|
|
42
|
+
from .models import (
|
|
43
|
+
ALL_MODELS,
|
|
44
|
+
MODELS_BY_ID,
|
|
45
|
+
Anthropic,
|
|
46
|
+
Cohere,
|
|
47
|
+
Gemini,
|
|
48
|
+
ModelInfo,
|
|
49
|
+
ModelType,
|
|
50
|
+
Ollama,
|
|
51
|
+
OpenAI,
|
|
52
|
+
)
|
|
53
|
+
from .observer import AgentObserver, AsyncAgentObserver, LoggingObserver
|
|
44
54
|
from .parser import ToolCallParser
|
|
45
55
|
from .policy import PolicyDecision, PolicyResult, ToolPolicy
|
|
46
56
|
from .pricing import PRICING, calculate_cost, calculate_embedding_cost, get_model_pricing
|
|
@@ -60,7 +70,7 @@ from .sessions import (
|
|
|
60
70
|
)
|
|
61
71
|
from .structured import ResponseFormat
|
|
62
72
|
from .tools import Tool, ToolParameter, ToolRegistry, tool
|
|
63
|
-
from .trace import AgentTrace, TraceStep
|
|
73
|
+
from .trace import AgentTrace, StepType, TraceStep
|
|
64
74
|
from .types import AgentResult, Message, Role, ToolCall
|
|
65
75
|
from .usage import AgentUsage, UsageStats
|
|
66
76
|
|
|
@@ -104,6 +114,7 @@ __all__ = [
|
|
|
104
114
|
# Model Registry
|
|
105
115
|
"models",
|
|
106
116
|
"ModelInfo",
|
|
117
|
+
"ModelType",
|
|
107
118
|
"ALL_MODELS",
|
|
108
119
|
"MODELS_BY_ID",
|
|
109
120
|
"OpenAI",
|
|
@@ -124,8 +135,10 @@ __all__ = [
|
|
|
124
135
|
"ResponseFormat",
|
|
125
136
|
# Observability
|
|
126
137
|
"AgentObserver",
|
|
138
|
+
"AsyncAgentObserver",
|
|
127
139
|
"LoggingObserver",
|
|
128
140
|
"AgentTrace",
|
|
141
|
+
"StepType",
|
|
129
142
|
"TraceStep",
|
|
130
143
|
# Guardrails
|
|
131
144
|
"guardrails",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Mixin providing observer notification and fallback wiring methods for the Agent class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import threading
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..memory import ConversationMemory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _LifecycleMixin:
|
|
14
|
+
"""Mixin that provides observer lifecycle methods for the Agent class.
|
|
15
|
+
|
|
16
|
+
All methods access ``self.*`` attributes (config, provider, etc.)
|
|
17
|
+
which are expected to be provided by the Agent class that inherits
|
|
18
|
+
from this mixin.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def _notify_observers(self, method: str, *args: Any) -> None:
|
|
22
|
+
"""Call *method* on every registered observer, swallowing errors."""
|
|
23
|
+
for obs in self.config.observers:
|
|
24
|
+
try:
|
|
25
|
+
getattr(obs, method)(*args)
|
|
26
|
+
except Exception: # noqa: BLE001 # nosec B110
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
async def _anotify_observers(self, method: str, *args: Any) -> None:
|
|
30
|
+
"""Call async ``a_{method}`` on every AsyncAgentObserver, swallowing errors.
|
|
31
|
+
|
|
32
|
+
- ``blocking=True`` observers: awaited inline (sequential).
|
|
33
|
+
- ``blocking=False`` observers: dispatched via ``asyncio.ensure_future``.
|
|
34
|
+
"""
|
|
35
|
+
from ..observer import AsyncAgentObserver
|
|
36
|
+
|
|
37
|
+
async_method = f"a_{method}"
|
|
38
|
+
for obs in self.config.observers:
|
|
39
|
+
if not isinstance(obs, AsyncAgentObserver):
|
|
40
|
+
continue
|
|
41
|
+
handler = getattr(obs, async_method, None)
|
|
42
|
+
if handler is None:
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
if obs.blocking:
|
|
46
|
+
await handler(*args)
|
|
47
|
+
else:
|
|
48
|
+
asyncio.ensure_future(handler(*args))
|
|
49
|
+
except Exception: # noqa: BLE001 # nosec B110
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def _truncate_tool_result(self, result: Optional[str]) -> Optional[str]:
|
|
53
|
+
"""Truncate tool result text for trace storage."""
|
|
54
|
+
if result is None:
|
|
55
|
+
return None
|
|
56
|
+
limit = self.config.trace_tool_result_chars
|
|
57
|
+
if limit is None:
|
|
58
|
+
return result
|
|
59
|
+
return result[:limit]
|
|
60
|
+
|
|
61
|
+
_fallback_run_id: threading.local = threading.local()
|
|
62
|
+
_lock_type: type = type(threading.Lock())
|
|
63
|
+
|
|
64
|
+
def _wire_fallback_observer(self, run_id: Optional[str]) -> None:
|
|
65
|
+
"""If the provider is a FallbackProvider, wire its on_fallback to observers.
|
|
66
|
+
|
|
67
|
+
Thread-safe: uses a lock + refcount so multiple concurrent ``run()``
|
|
68
|
+
calls (e.g. from ``batch()``) share a single callback on the provider
|
|
69
|
+
while each thread's ``run_id`` is carried via a thread-local.
|
|
70
|
+
The lock persists on the provider to avoid races from delete-then-recreate.
|
|
71
|
+
"""
|
|
72
|
+
if not run_id or not self.config.observers:
|
|
73
|
+
return
|
|
74
|
+
provider = self.provider
|
|
75
|
+
if not hasattr(provider, "on_fallback"):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
_LifecycleMixin._fallback_run_id.value = run_id
|
|
79
|
+
|
|
80
|
+
raw_lock = getattr(provider, "_fb_wire_lock", None)
|
|
81
|
+
if not isinstance(raw_lock, _LifecycleMixin._lock_type):
|
|
82
|
+
raw_lock = threading.Lock()
|
|
83
|
+
provider._fb_wire_lock = raw_lock # type: ignore[attr-defined]
|
|
84
|
+
lock = cast(threading.Lock, raw_lock)
|
|
85
|
+
|
|
86
|
+
agent_ref = self
|
|
87
|
+
|
|
88
|
+
with lock:
|
|
89
|
+
refcount: int = getattr(provider, "_fb_wire_refcount", 0)
|
|
90
|
+
if refcount == 0:
|
|
91
|
+
provider._fb_original_on_fallback = provider.on_fallback # type: ignore[attr-defined]
|
|
92
|
+
user_cb = provider.on_fallback
|
|
93
|
+
|
|
94
|
+
def _observer_fallback(
|
|
95
|
+
failed: str,
|
|
96
|
+
next_p: str,
|
|
97
|
+
exc: Exception,
|
|
98
|
+
) -> None:
|
|
99
|
+
rid = getattr(_LifecycleMixin._fallback_run_id, "value", "")
|
|
100
|
+
agent_ref._notify_observers(
|
|
101
|
+
"on_provider_fallback",
|
|
102
|
+
rid,
|
|
103
|
+
failed,
|
|
104
|
+
next_p,
|
|
105
|
+
exc,
|
|
106
|
+
)
|
|
107
|
+
if user_cb:
|
|
108
|
+
try:
|
|
109
|
+
user_cb(failed, next_p, exc)
|
|
110
|
+
except Exception: # nosec B110
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
provider.on_fallback = _observer_fallback # type: ignore[attr-defined]
|
|
114
|
+
|
|
115
|
+
provider._fb_wire_refcount = refcount + 1 # type: ignore[attr-defined]
|
|
116
|
+
|
|
117
|
+
def _unwire_fallback_observer(self) -> None:
|
|
118
|
+
"""Restore FallbackProvider's original on_fallback callback (thread-safe).
|
|
119
|
+
|
|
120
|
+
The lock is kept on the provider (never deleted) to prevent races when
|
|
121
|
+
concurrent threads overlap wire / unwire calls.
|
|
122
|
+
"""
|
|
123
|
+
provider = self.provider
|
|
124
|
+
raw_lock = getattr(provider, "_fb_wire_lock", None)
|
|
125
|
+
if not isinstance(raw_lock, _LifecycleMixin._lock_type):
|
|
126
|
+
return
|
|
127
|
+
lock = cast(threading.Lock, raw_lock)
|
|
128
|
+
|
|
129
|
+
with lock:
|
|
130
|
+
refcount: int = getattr(provider, "_fb_wire_refcount", 0) - 1
|
|
131
|
+
if refcount < 0:
|
|
132
|
+
refcount = 0
|
|
133
|
+
provider._fb_wire_refcount = refcount # type: ignore[attr-defined]
|
|
134
|
+
if refcount == 0:
|
|
135
|
+
original = getattr(provider, "_fb_original_on_fallback", None)
|
|
136
|
+
provider.on_fallback = original # type: ignore[attr-defined]
|
|
137
|
+
if hasattr(provider, "_fb_original_on_fallback"):
|
|
138
|
+
try:
|
|
139
|
+
delattr(provider, "_fb_original_on_fallback")
|
|
140
|
+
except Exception: # nosec B110
|
|
141
|
+
pass
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Mixin providing memory management methods for the Agent class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..types import Message, Role
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..memory import ConversationMemory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _MemoryManagerMixin:
|
|
14
|
+
"""Mixin that provides memory management methods for the Agent class.
|
|
15
|
+
|
|
16
|
+
All methods access ``self.*`` attributes (config, provider, memory,
|
|
17
|
+
_history, _notify_observers, etc.) which are expected to be provided
|
|
18
|
+
by the Agent class that inherits from this mixin.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def _memory_add(self, msg: Message, run_id: str) -> None:
|
|
22
|
+
"""Add message to memory and notify observers if trimming occurred."""
|
|
23
|
+
if not self.memory:
|
|
24
|
+
return
|
|
25
|
+
before = len(self.memory)
|
|
26
|
+
self.memory.add(msg)
|
|
27
|
+
after = len(self.memory)
|
|
28
|
+
removed = (before + 1) - after
|
|
29
|
+
if removed > 0:
|
|
30
|
+
self._notify_observers(
|
|
31
|
+
"on_memory_trim",
|
|
32
|
+
run_id,
|
|
33
|
+
removed,
|
|
34
|
+
after,
|
|
35
|
+
"enforce_limits",
|
|
36
|
+
)
|
|
37
|
+
self._maybe_summarize_trim(run_id)
|
|
38
|
+
|
|
39
|
+
def _memory_add_many(self, msgs: List[Message], run_id: str) -> None:
|
|
40
|
+
"""Add multiple messages to memory and notify observers if trimming occurred."""
|
|
41
|
+
if not self.memory or not msgs:
|
|
42
|
+
return
|
|
43
|
+
before = len(self.memory)
|
|
44
|
+
self.memory.add_many(msgs)
|
|
45
|
+
after = len(self.memory)
|
|
46
|
+
removed = (before + len(msgs)) - after
|
|
47
|
+
if removed > 0:
|
|
48
|
+
self._notify_observers(
|
|
49
|
+
"on_memory_trim",
|
|
50
|
+
run_id,
|
|
51
|
+
removed,
|
|
52
|
+
after,
|
|
53
|
+
"enforce_limits",
|
|
54
|
+
)
|
|
55
|
+
self._maybe_summarize_trim(run_id)
|
|
56
|
+
|
|
57
|
+
def _maybe_summarize_trim(self, run_id: str) -> None:
|
|
58
|
+
"""Generate a summary of trimmed messages if summarize_on_trim is enabled."""
|
|
59
|
+
if not self.config.summarize_on_trim or not self.memory:
|
|
60
|
+
return
|
|
61
|
+
trimmed = self.memory._last_trimmed
|
|
62
|
+
if not trimmed:
|
|
63
|
+
return
|
|
64
|
+
try:
|
|
65
|
+
provider = self.config.summarize_provider or self.provider
|
|
66
|
+
model = self.config.summarize_model or self.config.model
|
|
67
|
+
text_parts = []
|
|
68
|
+
for m in trimmed:
|
|
69
|
+
prefix = m.role.value.upper()
|
|
70
|
+
text_parts.append(f"{prefix}: {m.content or ''}")
|
|
71
|
+
trimmed_text = "\n".join(text_parts)
|
|
72
|
+
|
|
73
|
+
prompt_msg = Message(
|
|
74
|
+
role=Role.USER,
|
|
75
|
+
content=(
|
|
76
|
+
"Summarize the following conversation excerpt in 2-3 sentences. "
|
|
77
|
+
"Focus on key facts, decisions, and context that would be useful "
|
|
78
|
+
"for continuing the conversation:\n\n" + trimmed_text
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
result = provider.complete(
|
|
82
|
+
model=model,
|
|
83
|
+
system_prompt="You are a concise summarizer.",
|
|
84
|
+
messages=[prompt_msg],
|
|
85
|
+
max_tokens=self.config.summarize_max_tokens,
|
|
86
|
+
)
|
|
87
|
+
# Provider returns (Message, UsageStats) tuple
|
|
88
|
+
summary_msg = result[0] if isinstance(result, tuple) else result
|
|
89
|
+
summary_text = summary_msg.content or ""
|
|
90
|
+
if summary_text:
|
|
91
|
+
existing = self.memory.summary
|
|
92
|
+
if existing:
|
|
93
|
+
self.memory.summary = existing + " " + summary_text
|
|
94
|
+
else:
|
|
95
|
+
self.memory.summary = summary_text
|
|
96
|
+
self._notify_observers("on_memory_summarize", run_id, self.memory.summary)
|
|
97
|
+
except Exception: # nosec B110
|
|
98
|
+
pass # never crash the agent for a summarization failure
|
|
99
|
+
|
|
100
|
+
def _session_save(self, run_id: str) -> None:
|
|
101
|
+
"""Auto-save memory to session store if configured."""
|
|
102
|
+
store = self.config.session_store
|
|
103
|
+
sid = self.config.session_id
|
|
104
|
+
if not store or not sid or not self.memory:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
store.save(sid, self.memory)
|
|
108
|
+
self._notify_observers("on_session_save", run_id, sid, len(self.memory))
|
|
109
|
+
except Exception: # nosec B110
|
|
110
|
+
pass # never crash the agent for a persistence failure
|
|
111
|
+
|
|
112
|
+
def _extract_entities(self, run_id: str) -> None:
|
|
113
|
+
"""Extract entities from recent messages if entity_memory is configured."""
|
|
114
|
+
em = self.config.entity_memory
|
|
115
|
+
if not em:
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
recent = self._history[-em._relevance_window :]
|
|
119
|
+
entities = em.extract_entities(recent, model=self.config.model)
|
|
120
|
+
if entities:
|
|
121
|
+
em.update(entities)
|
|
122
|
+
self._notify_observers(
|
|
123
|
+
"on_entity_extraction",
|
|
124
|
+
run_id,
|
|
125
|
+
len(entities),
|
|
126
|
+
)
|
|
127
|
+
except Exception: # nosec B110
|
|
128
|
+
pass # never crash the agent for entity extraction failure
|
|
129
|
+
|
|
130
|
+
def _extract_kg_triples(self, run_id: str) -> None:
|
|
131
|
+
"""Extract relationship triples from recent messages if knowledge_graph is configured."""
|
|
132
|
+
kg = self.config.knowledge_graph
|
|
133
|
+
if not kg:
|
|
134
|
+
return
|
|
135
|
+
try:
|
|
136
|
+
recent = self._history[-kg._relevance_window :]
|
|
137
|
+
triples = kg.extract_triples(recent, model=self.config.model)
|
|
138
|
+
if triples:
|
|
139
|
+
kg.store.add_many(triples)
|
|
140
|
+
self._notify_observers(
|
|
141
|
+
"on_kg_extraction",
|
|
142
|
+
run_id,
|
|
143
|
+
len(triples),
|
|
144
|
+
)
|
|
145
|
+
except Exception: # nosec B110
|
|
146
|
+
pass # never crash the agent for KG extraction failure
|