kcs-agent 0.1.0__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.
- kcs_agent-0.1.0/.gitignore +9 -0
- kcs_agent-0.1.0/LICENSE +21 -0
- kcs_agent-0.1.0/PKG-INFO +227 -0
- kcs_agent-0.1.0/README.md +202 -0
- kcs_agent-0.1.0/pyproject.toml +68 -0
- kcs_agent-0.1.0/src/kcs_agent/__init__.py +175 -0
- kcs_agent-0.1.0/src/kcs_agent/agent.py +409 -0
- kcs_agent-0.1.0/src/kcs_agent/events.py +79 -0
- kcs_agent-0.1.0/src/kcs_agent/exceptions.py +26 -0
- kcs_agent-0.1.0/src/kcs_agent/extensions.py +170 -0
- kcs_agent-0.1.0/src/kcs_agent/messages.py +240 -0
- kcs_agent-0.1.0/src/kcs_agent/model.py +127 -0
- kcs_agent-0.1.0/src/kcs_agent/providers.py +999 -0
- kcs_agent-0.1.0/src/kcs_agent/py.typed +0 -0
- kcs_agent-0.1.0/src/kcs_agent/schema.py +225 -0
- kcs_agent-0.1.0/src/kcs_agent/state.py +210 -0
- kcs_agent-0.1.0/src/kcs_agent/tools.py +331 -0
kcs_agent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chang-LeHung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kcs_agent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kcs-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A provider-neutral agent loop with extension hooks and typed tools
|
|
5
|
+
Project-URL: Repository, https://github.com/Chang-LeHung/knowledge-cards-system
|
|
6
|
+
Author: Chang-LeHung
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,extensions,llm,streaming,tools
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: anthropic<1,>=0.125
|
|
18
|
+
Requires-Dist: google-genai<3,>=2.22
|
|
19
|
+
Requires-Dist: httpx<1,>=0.27
|
|
20
|
+
Requires-Dist: ollama<1,>=0.6.2
|
|
21
|
+
Requires-Dist: openai<3,>=2.54
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
23
|
+
Requires-Dist: truststore<1,>=0.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# KCS Agent
|
|
27
|
+
|
|
28
|
+
A typed, provider-neutral Python agent loop with streaming output and composable extensions.
|
|
29
|
+
Python **3.12+** · **MIT** · No LangChain dependency.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add kcs-agent
|
|
35
|
+
# or: pip install kcs-agent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
from kcs_agent import Agent, DeepSeekProvider, ReasoningEffort, UserMessage, UserMessageData
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def main() -> None:
|
|
48
|
+
model = DeepSeekProvider(model="deepseek-v4-flash", api_key=os.environ["DEEPSEEK_API"])
|
|
49
|
+
try:
|
|
50
|
+
result = await Agent(model).run(
|
|
51
|
+
UserMessage(data=UserMessageData("Explain an agent loop in one sentence.")),
|
|
52
|
+
reasoning_effort=ReasoningEffort.OFF,
|
|
53
|
+
)
|
|
54
|
+
print(result.message.data.content)
|
|
55
|
+
finally:
|
|
56
|
+
await model.aclose()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each invocation supplies the **latest input**. The core does not retrieve earlier conversations.
|
|
63
|
+
It owns the bounded model/tool loop, ordered events, cancellation state, and usage accumulation.
|
|
64
|
+
|
|
65
|
+
## Tools and system-prompt guidance
|
|
66
|
+
|
|
67
|
+
A tool remains a regular typed function. Its first docstring paragraph is its description.
|
|
68
|
+
The bare `@tool` form reads `Snippet:`, `Guidelines:`, and `Args:` from the docstring:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from kcs_agent import Agent, ToolPromptExtension, tool
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@tool
|
|
75
|
+
def add(left: int, right: int) -> int:
|
|
76
|
+
"""Add two integer values.
|
|
77
|
+
|
|
78
|
+
Snippet:
|
|
79
|
+
add(left, right) -> sum
|
|
80
|
+
|
|
81
|
+
Guidelines:
|
|
82
|
+
- Use for exact integer addition.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
left: First integer.
|
|
86
|
+
right: Second integer.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
>>> add(2, 3)
|
|
90
|
+
5
|
|
91
|
+
"""
|
|
92
|
+
return left + right
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_agent(model):
|
|
96
|
+
return Agent(
|
|
97
|
+
model,
|
|
98
|
+
system_prompt="Answer accurately.",
|
|
99
|
+
extensions=[ToolPromptExtension([add])],
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The configured form, `@tool(snippet="...", guideline="...")`, overrides docstring guidance.
|
|
104
|
+
`Examples:` documents Python usage; it is never inserted into the model prompt.
|
|
105
|
+
|
|
106
|
+
`ToolPromptExtension` registers tools and appends their guidance to the **end of the combined
|
|
107
|
+
system instructions**, first all snippets, then all guidelines. It derives each model request
|
|
108
|
+
from unchanged run messages, preventing duplicate guidance in repeated tool rounds.
|
|
109
|
+
Place it after other prompt-transforming extensions.
|
|
110
|
+
|
|
111
|
+
`ToolExtension` registers tools without modifying the prompt. `parse_tool(function)` exports
|
|
112
|
+
the callable's input and return JSON Schemas. Arguments are validated and nested Pydantic
|
|
113
|
+
models are constructed before invocation. Synchronous tools run in a worker thread.
|
|
114
|
+
Cancellation does not forcibly terminate an already-running synchronous function;
|
|
115
|
+
applications must provide cooperative cancellation for long-running side effects.
|
|
116
|
+
|
|
117
|
+
## History extension
|
|
118
|
+
|
|
119
|
+
`HistoryExtension` invokes an async loader once per run. The loader receives typed
|
|
120
|
+
`AgentContext[SessionDataT]`, including the stable session ID. It returns preceding messages,
|
|
121
|
+
optionally a context snapshot followed by its replay tail. **Exclude the current input**,
|
|
122
|
+
even if the application already persisted it.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from kcs_agent import (
|
|
126
|
+
Agent, AgentContext, AnyMessage, HistoryExtension, SessionState,
|
|
127
|
+
ToolPromptExtension, UserMessage, UserMessageData,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
history: dict[str, list[AnyMessage]] = {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def load_history(context: AgentContext[None]) -> list[AnyMessage]:
|
|
134
|
+
return list(history.get(context.state.session.session_id, ()))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def conversation(model) -> None:
|
|
138
|
+
session = SessionState(data=None)
|
|
139
|
+
agent = Agent(model, extensions=[HistoryExtension(load_history), ToolPromptExtension([add])])
|
|
140
|
+
for text in ("Use add to compute 2+3.", "What was the result?"):
|
|
141
|
+
result = await agent.run(UserMessage(data=UserMessageData(text)), session=session)
|
|
142
|
+
history[session.session_id] = [
|
|
143
|
+
message for message in result.state.run.messages if message.role != "system"
|
|
144
|
+
]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This example stores history in memory. Production applications own durable storage and
|
|
148
|
+
compaction through the loader and lifecycle hooks. Different sessions never share
|
|
149
|
+
mutable run state through the agent instance.
|
|
150
|
+
|
|
151
|
+
## Streaming and hooks
|
|
152
|
+
|
|
153
|
+
`Agent.stream(...)` emits ordered `AgentEvent` values. Relevant event types include:
|
|
154
|
+
|
|
155
|
+
- `TEXT_DELTA`, `REASONING_DELTA`, and `TOOL_CALL_DELTA`
|
|
156
|
+
- `MODEL_STARTED` and `MODEL_COMPLETED`
|
|
157
|
+
- `TOOL_STARTED`, `TOOL_COMPLETED`, and `TOOL_FAILED`
|
|
158
|
+
- `RUN_COMPLETED`, `RUN_FAILED`, and cooperative `RUN_CANCELLED`
|
|
159
|
+
|
|
160
|
+
Tool deltas are display-only fragments. The final model response contains authoritative
|
|
161
|
+
complete tool calls. Models may answer without calling any tool.
|
|
162
|
+
|
|
163
|
+
An extension can implement `load_state`, `on_run_start`, `context_messages`, `tools`,
|
|
164
|
+
`before_model`, `after_model`, `before_tool`, `after_tool`, `on_message`,
|
|
165
|
+
`on_checkpoint`, `on_error`, `on_run_end`, and `release_state`.
|
|
166
|
+
Hooks execute in registration order. `before_model` may return a replacement immutable
|
|
167
|
+
`ModelRequest`; returning `None` leaves it unchanged. `on_message` observes generated
|
|
168
|
+
assistant/tool messages; the application owns persistence of caller-supplied input.
|
|
169
|
+
|
|
170
|
+
Task cancellation and closing the stream checkpoint partial state and release resources.
|
|
171
|
+
Task cancellation propagates `asyncio.CancelledError`; it cannot yield a terminal event to
|
|
172
|
+
an already disconnected consumer.
|
|
173
|
+
|
|
174
|
+
## Messages and providers
|
|
175
|
+
|
|
176
|
+
Messages separate semantic data from typed metadata using `Message[DataT, MetadataT]`.
|
|
177
|
+
Concrete roles are system, user, assistant, and tool. User content accepts text and ordered
|
|
178
|
+
image blocks with URL, bytes, or opaque asset sources. Applications must resolve opaque
|
|
179
|
+
asset IDs to actual images when visual understanding is required. Ollama accepts encoded
|
|
180
|
+
image bytes or base64 data URLs; remote images must be downloaded by the application first.
|
|
181
|
+
|
|
182
|
+
Official SDK adapters are included:
|
|
183
|
+
|
|
184
|
+
| Adapter | SDK | Notes |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `OpenAIProvider` | OpenAI | Chat Completions; configurable base URL and temperature |
|
|
187
|
+
| `DeepSeekProvider` | OpenAI | Thinking and reasoning replay; configurable base URL and temperature |
|
|
188
|
+
| `AnthropicProvider` | Anthropic | Configurable base URL; signed thinking and tool-result replay |
|
|
189
|
+
| `GoogleProvider` | Google GenAI | Text, images, function calls, and signed thinking parts |
|
|
190
|
+
| `OllamaProvider` | Ollama | Configurable local base URL; incremental NDJSON streaming |
|
|
191
|
+
|
|
192
|
+
Adapters use the operating system certificate trust store. Each accepts an optional
|
|
193
|
+
`httpx.AsyncBaseTransport` for isolated tests and exposes `aclose()`.
|
|
194
|
+
|
|
195
|
+
Reasoning effort is supplied per run using `ReasoningEffort`: `off`, `minimal`, `low`,
|
|
196
|
+
`medium`, `high`, and `xhigh`. Provider capabilities differ. DeepSeek V4 maps minimal/low
|
|
197
|
+
to low, medium/high to high, and xhigh to max. Anthropic and Google use token budgets.
|
|
198
|
+
OpenAI forwards enabled levels; unsupported model/level combinations may be rejected by
|
|
199
|
+
the provider. Ollama maps enabled levels to its boolean thinking switch.
|
|
200
|
+
|
|
201
|
+
For structured output, `ModelRequest.tool_choice` names a schema-bound response tool.
|
|
202
|
+
OpenAI, Anthropic, and Google force that choice; Ollama receives an explicit instruction.
|
|
203
|
+
Consumers must validate the completed tool arguments against their output model.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
uv sync
|
|
209
|
+
uv run ruff check src tests examples
|
|
210
|
+
uv run ruff format --check src tests examples
|
|
211
|
+
uv run pytest
|
|
212
|
+
|
|
213
|
+
# Real DeepSeek streams and tool round trips via both protocols:
|
|
214
|
+
KCS_AGENT_LIVE_TESTS=1 uv run pytest tests/test_live_providers.py
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Live tests require `DEEPSEEK_API` and default to `deepseek-v4-flash`.
|
|
218
|
+
Optional overrides: `DEEPSEEK_API_MODEL`, `DEEPSEEK_ANTHROPIC_MODEL`,
|
|
219
|
+
`DEEPSEEK_ANTHROPIC_BASE_URL`, and `DEEPSEEK_ANTHROPIC_API_KEY`.
|
|
220
|
+
Network and provider failures fail explicitly when live tests are enabled.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
uv build
|
|
224
|
+
uv publish dist/kcs_agent-0.1.0.tar.gz dist/kcs_agent-0.1.0-py3-none-any.whl
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Supply publishing credentials through your local credential store or `UV_PUBLISH_TOKEN`.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# KCS Agent
|
|
2
|
+
|
|
3
|
+
A typed, provider-neutral Python agent loop with streaming output and composable extensions.
|
|
4
|
+
Python **3.12+** · **MIT** · No LangChain dependency.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv add kcs-agent
|
|
10
|
+
# or: pip install kcs-agent
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from kcs_agent import Agent, DeepSeekProvider, ReasoningEffort, UserMessage, UserMessageData
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def main() -> None:
|
|
23
|
+
model = DeepSeekProvider(model="deepseek-v4-flash", api_key=os.environ["DEEPSEEK_API"])
|
|
24
|
+
try:
|
|
25
|
+
result = await Agent(model).run(
|
|
26
|
+
UserMessage(data=UserMessageData("Explain an agent loop in one sentence.")),
|
|
27
|
+
reasoning_effort=ReasoningEffort.OFF,
|
|
28
|
+
)
|
|
29
|
+
print(result.message.data.content)
|
|
30
|
+
finally:
|
|
31
|
+
await model.aclose()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
asyncio.run(main())
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each invocation supplies the **latest input**. The core does not retrieve earlier conversations.
|
|
38
|
+
It owns the bounded model/tool loop, ordered events, cancellation state, and usage accumulation.
|
|
39
|
+
|
|
40
|
+
## Tools and system-prompt guidance
|
|
41
|
+
|
|
42
|
+
A tool remains a regular typed function. Its first docstring paragraph is its description.
|
|
43
|
+
The bare `@tool` form reads `Snippet:`, `Guidelines:`, and `Args:` from the docstring:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from kcs_agent import Agent, ToolPromptExtension, tool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@tool
|
|
50
|
+
def add(left: int, right: int) -> int:
|
|
51
|
+
"""Add two integer values.
|
|
52
|
+
|
|
53
|
+
Snippet:
|
|
54
|
+
add(left, right) -> sum
|
|
55
|
+
|
|
56
|
+
Guidelines:
|
|
57
|
+
- Use for exact integer addition.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
left: First integer.
|
|
61
|
+
right: Second integer.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> add(2, 3)
|
|
65
|
+
5
|
|
66
|
+
"""
|
|
67
|
+
return left + right
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_agent(model):
|
|
71
|
+
return Agent(
|
|
72
|
+
model,
|
|
73
|
+
system_prompt="Answer accurately.",
|
|
74
|
+
extensions=[ToolPromptExtension([add])],
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The configured form, `@tool(snippet="...", guideline="...")`, overrides docstring guidance.
|
|
79
|
+
`Examples:` documents Python usage; it is never inserted into the model prompt.
|
|
80
|
+
|
|
81
|
+
`ToolPromptExtension` registers tools and appends their guidance to the **end of the combined
|
|
82
|
+
system instructions**, first all snippets, then all guidelines. It derives each model request
|
|
83
|
+
from unchanged run messages, preventing duplicate guidance in repeated tool rounds.
|
|
84
|
+
Place it after other prompt-transforming extensions.
|
|
85
|
+
|
|
86
|
+
`ToolExtension` registers tools without modifying the prompt. `parse_tool(function)` exports
|
|
87
|
+
the callable's input and return JSON Schemas. Arguments are validated and nested Pydantic
|
|
88
|
+
models are constructed before invocation. Synchronous tools run in a worker thread.
|
|
89
|
+
Cancellation does not forcibly terminate an already-running synchronous function;
|
|
90
|
+
applications must provide cooperative cancellation for long-running side effects.
|
|
91
|
+
|
|
92
|
+
## History extension
|
|
93
|
+
|
|
94
|
+
`HistoryExtension` invokes an async loader once per run. The loader receives typed
|
|
95
|
+
`AgentContext[SessionDataT]`, including the stable session ID. It returns preceding messages,
|
|
96
|
+
optionally a context snapshot followed by its replay tail. **Exclude the current input**,
|
|
97
|
+
even if the application already persisted it.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from kcs_agent import (
|
|
101
|
+
Agent, AgentContext, AnyMessage, HistoryExtension, SessionState,
|
|
102
|
+
ToolPromptExtension, UserMessage, UserMessageData,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
history: dict[str, list[AnyMessage]] = {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def load_history(context: AgentContext[None]) -> list[AnyMessage]:
|
|
109
|
+
return list(history.get(context.state.session.session_id, ()))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def conversation(model) -> None:
|
|
113
|
+
session = SessionState(data=None)
|
|
114
|
+
agent = Agent(model, extensions=[HistoryExtension(load_history), ToolPromptExtension([add])])
|
|
115
|
+
for text in ("Use add to compute 2+3.", "What was the result?"):
|
|
116
|
+
result = await agent.run(UserMessage(data=UserMessageData(text)), session=session)
|
|
117
|
+
history[session.session_id] = [
|
|
118
|
+
message for message in result.state.run.messages if message.role != "system"
|
|
119
|
+
]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This example stores history in memory. Production applications own durable storage and
|
|
123
|
+
compaction through the loader and lifecycle hooks. Different sessions never share
|
|
124
|
+
mutable run state through the agent instance.
|
|
125
|
+
|
|
126
|
+
## Streaming and hooks
|
|
127
|
+
|
|
128
|
+
`Agent.stream(...)` emits ordered `AgentEvent` values. Relevant event types include:
|
|
129
|
+
|
|
130
|
+
- `TEXT_DELTA`, `REASONING_DELTA`, and `TOOL_CALL_DELTA`
|
|
131
|
+
- `MODEL_STARTED` and `MODEL_COMPLETED`
|
|
132
|
+
- `TOOL_STARTED`, `TOOL_COMPLETED`, and `TOOL_FAILED`
|
|
133
|
+
- `RUN_COMPLETED`, `RUN_FAILED`, and cooperative `RUN_CANCELLED`
|
|
134
|
+
|
|
135
|
+
Tool deltas are display-only fragments. The final model response contains authoritative
|
|
136
|
+
complete tool calls. Models may answer without calling any tool.
|
|
137
|
+
|
|
138
|
+
An extension can implement `load_state`, `on_run_start`, `context_messages`, `tools`,
|
|
139
|
+
`before_model`, `after_model`, `before_tool`, `after_tool`, `on_message`,
|
|
140
|
+
`on_checkpoint`, `on_error`, `on_run_end`, and `release_state`.
|
|
141
|
+
Hooks execute in registration order. `before_model` may return a replacement immutable
|
|
142
|
+
`ModelRequest`; returning `None` leaves it unchanged. `on_message` observes generated
|
|
143
|
+
assistant/tool messages; the application owns persistence of caller-supplied input.
|
|
144
|
+
|
|
145
|
+
Task cancellation and closing the stream checkpoint partial state and release resources.
|
|
146
|
+
Task cancellation propagates `asyncio.CancelledError`; it cannot yield a terminal event to
|
|
147
|
+
an already disconnected consumer.
|
|
148
|
+
|
|
149
|
+
## Messages and providers
|
|
150
|
+
|
|
151
|
+
Messages separate semantic data from typed metadata using `Message[DataT, MetadataT]`.
|
|
152
|
+
Concrete roles are system, user, assistant, and tool. User content accepts text and ordered
|
|
153
|
+
image blocks with URL, bytes, or opaque asset sources. Applications must resolve opaque
|
|
154
|
+
asset IDs to actual images when visual understanding is required. Ollama accepts encoded
|
|
155
|
+
image bytes or base64 data URLs; remote images must be downloaded by the application first.
|
|
156
|
+
|
|
157
|
+
Official SDK adapters are included:
|
|
158
|
+
|
|
159
|
+
| Adapter | SDK | Notes |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `OpenAIProvider` | OpenAI | Chat Completions; configurable base URL and temperature |
|
|
162
|
+
| `DeepSeekProvider` | OpenAI | Thinking and reasoning replay; configurable base URL and temperature |
|
|
163
|
+
| `AnthropicProvider` | Anthropic | Configurable base URL; signed thinking and tool-result replay |
|
|
164
|
+
| `GoogleProvider` | Google GenAI | Text, images, function calls, and signed thinking parts |
|
|
165
|
+
| `OllamaProvider` | Ollama | Configurable local base URL; incremental NDJSON streaming |
|
|
166
|
+
|
|
167
|
+
Adapters use the operating system certificate trust store. Each accepts an optional
|
|
168
|
+
`httpx.AsyncBaseTransport` for isolated tests and exposes `aclose()`.
|
|
169
|
+
|
|
170
|
+
Reasoning effort is supplied per run using `ReasoningEffort`: `off`, `minimal`, `low`,
|
|
171
|
+
`medium`, `high`, and `xhigh`. Provider capabilities differ. DeepSeek V4 maps minimal/low
|
|
172
|
+
to low, medium/high to high, and xhigh to max. Anthropic and Google use token budgets.
|
|
173
|
+
OpenAI forwards enabled levels; unsupported model/level combinations may be rejected by
|
|
174
|
+
the provider. Ollama maps enabled levels to its boolean thinking switch.
|
|
175
|
+
|
|
176
|
+
For structured output, `ModelRequest.tool_choice` names a schema-bound response tool.
|
|
177
|
+
OpenAI, Anthropic, and Google force that choice; Ollama receives an explicit instruction.
|
|
178
|
+
Consumers must validate the completed tool arguments against their output model.
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv sync
|
|
184
|
+
uv run ruff check src tests examples
|
|
185
|
+
uv run ruff format --check src tests examples
|
|
186
|
+
uv run pytest
|
|
187
|
+
|
|
188
|
+
# Real DeepSeek streams and tool round trips via both protocols:
|
|
189
|
+
KCS_AGENT_LIVE_TESTS=1 uv run pytest tests/test_live_providers.py
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Live tests require `DEEPSEEK_API` and default to `deepseek-v4-flash`.
|
|
193
|
+
Optional overrides: `DEEPSEEK_API_MODEL`, `DEEPSEEK_ANTHROPIC_MODEL`,
|
|
194
|
+
`DEEPSEEK_ANTHROPIC_BASE_URL`, and `DEEPSEEK_ANTHROPIC_API_KEY`.
|
|
195
|
+
Network and provider failures fail explicitly when live tests are enabled.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv build
|
|
199
|
+
uv publish dist/kcs_agent-0.1.0.tar.gz dist/kcs_agent-0.1.0-py3-none-any.whl
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Supply publishing credentials through your local credential store or `UV_PUBLISH_TOKEN`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kcs-agent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A provider-neutral agent loop with extension hooks and typed tools"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{name = "Chang-LeHung"}]
|
|
10
|
+
keywords = ["agent", "llm", "streaming", "tools", "extensions"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.27,<1",
|
|
21
|
+
"pydantic>=2.12,<3",
|
|
22
|
+
"truststore>=0.10,<1",
|
|
23
|
+
"openai>=2.54,<3",
|
|
24
|
+
"anthropic>=0.125,<1",
|
|
25
|
+
"google-genai>=2.22,<3",
|
|
26
|
+
"ollama>=0.6.2,<1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Repository = "https://github.com/Chang-LeHung/knowledge-cards-system"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/kcs_agent"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
include = ["/src", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8,<9",
|
|
45
|
+
"pytest-asyncio>=0.26,<2",
|
|
46
|
+
"ruff>=0.11,<1",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 120
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
src = ["src", "tests", "examples"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
60
|
+
ignore = ["E501"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.isort]
|
|
63
|
+
known-first-party = ["kcs_agent"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.format]
|
|
66
|
+
quote-style = "double"
|
|
67
|
+
indent-style = "space"
|
|
68
|
+
line-ending = "lf"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from .agent import Agent
|
|
2
|
+
from .events import AgentEvent, AgentEventType, AgentResult
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
AgentCancelledError,
|
|
5
|
+
AgentError,
|
|
6
|
+
AgentIterationLimitError,
|
|
7
|
+
AgentProtocolError,
|
|
8
|
+
SessionVersionConflictError,
|
|
9
|
+
ToolDefinitionError,
|
|
10
|
+
ToolInvocationError,
|
|
11
|
+
)
|
|
12
|
+
from .extensions import (
|
|
13
|
+
AgentContext,
|
|
14
|
+
AgentExtension,
|
|
15
|
+
ExtensionFailurePolicy,
|
|
16
|
+
HistoryExtension,
|
|
17
|
+
StateExtension,
|
|
18
|
+
StaticContextExtension,
|
|
19
|
+
ToolExtension,
|
|
20
|
+
ToolPromptExtension,
|
|
21
|
+
)
|
|
22
|
+
from .messages import (
|
|
23
|
+
AnyMessage,
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
AssistantMessageData,
|
|
26
|
+
AssistantMessageMetadata,
|
|
27
|
+
ImageAssetSource,
|
|
28
|
+
ImageBytesSource,
|
|
29
|
+
ImageContent,
|
|
30
|
+
ImageDetail,
|
|
31
|
+
ImageSource,
|
|
32
|
+
ImageUrlSource,
|
|
33
|
+
Message,
|
|
34
|
+
MessageRole,
|
|
35
|
+
SystemMessage,
|
|
36
|
+
SystemMessageData,
|
|
37
|
+
SystemMessageMetadata,
|
|
38
|
+
TextContent,
|
|
39
|
+
ToolCall,
|
|
40
|
+
ToolMessage,
|
|
41
|
+
ToolMessageData,
|
|
42
|
+
ToolMessageMetadata,
|
|
43
|
+
UserContent,
|
|
44
|
+
UserContentPart,
|
|
45
|
+
UserMessage,
|
|
46
|
+
UserMessageData,
|
|
47
|
+
UserMessageMetadata,
|
|
48
|
+
)
|
|
49
|
+
from .model import (
|
|
50
|
+
AgentModel,
|
|
51
|
+
ModelEvent,
|
|
52
|
+
ModelEventType,
|
|
53
|
+
ModelRequest,
|
|
54
|
+
ModelResponse,
|
|
55
|
+
ModelUsage,
|
|
56
|
+
ReasoningEffort,
|
|
57
|
+
ToolCallDelta,
|
|
58
|
+
ToolDefinition,
|
|
59
|
+
)
|
|
60
|
+
from .providers import (
|
|
61
|
+
AnthropicProvider,
|
|
62
|
+
DeepSeekProvider,
|
|
63
|
+
GoogleProvider,
|
|
64
|
+
OllamaProvider,
|
|
65
|
+
OpenAIProvider,
|
|
66
|
+
ProviderAuthError,
|
|
67
|
+
ProviderError,
|
|
68
|
+
ProviderResponseError,
|
|
69
|
+
)
|
|
70
|
+
from .schema import Parameter, annotation_schema, callable_schema
|
|
71
|
+
from .state import (
|
|
72
|
+
AgentErrorInfo,
|
|
73
|
+
AgentState,
|
|
74
|
+
CancellationToken,
|
|
75
|
+
CheckpointReason,
|
|
76
|
+
ExtensionStateRegistry,
|
|
77
|
+
RunState,
|
|
78
|
+
RunStatus,
|
|
79
|
+
SessionState,
|
|
80
|
+
ToolCallStreamState,
|
|
81
|
+
UsageState,
|
|
82
|
+
)
|
|
83
|
+
from .tools import (
|
|
84
|
+
AgentTool,
|
|
85
|
+
ToolDocstring,
|
|
86
|
+
ToolParameterDocumentation,
|
|
87
|
+
parse_tool,
|
|
88
|
+
parse_tool_docstring,
|
|
89
|
+
render_tool_guidance,
|
|
90
|
+
tool,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"Agent",
|
|
95
|
+
"AgentContext",
|
|
96
|
+
"AgentCancelledError",
|
|
97
|
+
"AgentError",
|
|
98
|
+
"AgentErrorInfo",
|
|
99
|
+
"AgentEvent",
|
|
100
|
+
"AgentEventType",
|
|
101
|
+
"AgentExtension",
|
|
102
|
+
"AgentIterationLimitError",
|
|
103
|
+
"AgentModel",
|
|
104
|
+
"AgentProtocolError",
|
|
105
|
+
"AgentResult",
|
|
106
|
+
"AgentState",
|
|
107
|
+
"AgentTool",
|
|
108
|
+
"AnyMessage",
|
|
109
|
+
"AssistantMessage",
|
|
110
|
+
"AssistantMessageData",
|
|
111
|
+
"AssistantMessageMetadata",
|
|
112
|
+
"CancellationToken",
|
|
113
|
+
"CheckpointReason",
|
|
114
|
+
"ExtensionFailurePolicy",
|
|
115
|
+
"ExtensionStateRegistry",
|
|
116
|
+
"HistoryExtension",
|
|
117
|
+
"ImageAssetSource",
|
|
118
|
+
"ImageBytesSource",
|
|
119
|
+
"ImageContent",
|
|
120
|
+
"ImageDetail",
|
|
121
|
+
"ImageSource",
|
|
122
|
+
"ImageUrlSource",
|
|
123
|
+
"Message",
|
|
124
|
+
"MessageRole",
|
|
125
|
+
"ModelEvent",
|
|
126
|
+
"ModelEventType",
|
|
127
|
+
"ModelRequest",
|
|
128
|
+
"ModelResponse",
|
|
129
|
+
"ModelUsage",
|
|
130
|
+
"Parameter",
|
|
131
|
+
"ReasoningEffort",
|
|
132
|
+
"RunState",
|
|
133
|
+
"RunStatus",
|
|
134
|
+
"SessionState",
|
|
135
|
+
"SessionVersionConflictError",
|
|
136
|
+
"StateExtension",
|
|
137
|
+
"StaticContextExtension",
|
|
138
|
+
"SystemMessage",
|
|
139
|
+
"SystemMessageData",
|
|
140
|
+
"SystemMessageMetadata",
|
|
141
|
+
"ToolCall",
|
|
142
|
+
"ToolCallDelta",
|
|
143
|
+
"ToolCallStreamState",
|
|
144
|
+
"ToolDefinition",
|
|
145
|
+
"ToolDefinitionError",
|
|
146
|
+
"ToolDocstring",
|
|
147
|
+
"ToolExtension",
|
|
148
|
+
"ToolPromptExtension",
|
|
149
|
+
"ToolInvocationError",
|
|
150
|
+
"ToolMessage",
|
|
151
|
+
"ToolMessageData",
|
|
152
|
+
"ToolMessageMetadata",
|
|
153
|
+
"ToolParameterDocumentation",
|
|
154
|
+
"TextContent",
|
|
155
|
+
"UsageState",
|
|
156
|
+
"annotation_schema",
|
|
157
|
+
"callable_schema",
|
|
158
|
+
"parse_tool",
|
|
159
|
+
"parse_tool_docstring",
|
|
160
|
+
"render_tool_guidance",
|
|
161
|
+
"AnthropicProvider",
|
|
162
|
+
"DeepSeekProvider",
|
|
163
|
+
"GoogleProvider",
|
|
164
|
+
"OllamaProvider",
|
|
165
|
+
"OpenAIProvider",
|
|
166
|
+
"ProviderAuthError",
|
|
167
|
+
"ProviderError",
|
|
168
|
+
"ProviderResponseError",
|
|
169
|
+
"tool",
|
|
170
|
+
"UserMessage",
|
|
171
|
+
"UserContent",
|
|
172
|
+
"UserContentPart",
|
|
173
|
+
"UserMessageData",
|
|
174
|
+
"UserMessageMetadata",
|
|
175
|
+
]
|