kimss-forge 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.
- kimss_forge-0.1.0/LICENSE +21 -0
- kimss_forge-0.1.0/PKG-INFO +139 -0
- kimss_forge-0.1.0/README.md +111 -0
- kimss_forge-0.1.0/kimss_forge/__init__.py +20 -0
- kimss_forge-0.1.0/kimss_forge/agent.py +111 -0
- kimss_forge-0.1.0/kimss_forge/client.py +81 -0
- kimss_forge-0.1.0/kimss_forge/gateway.py +68 -0
- kimss_forge-0.1.0/kimss_forge/loop.py +148 -0
- kimss_forge-0.1.0/kimss_forge/mcp.py +94 -0
- kimss_forge-0.1.0/kimss_forge/tools.py +131 -0
- kimss_forge-0.1.0/kimss_forge.egg-info/PKG-INFO +139 -0
- kimss_forge-0.1.0/kimss_forge.egg-info/SOURCES.txt +16 -0
- kimss_forge-0.1.0/kimss_forge.egg-info/dependency_links.txt +1 -0
- kimss_forge-0.1.0/kimss_forge.egg-info/requires.txt +9 -0
- kimss_forge-0.1.0/kimss_forge.egg-info/top_level.txt +1 -0
- kimss_forge-0.1.0/pyproject.toml +54 -0
- kimss_forge-0.1.0/setup.cfg +4 -0
- kimss_forge-0.1.0/tests/test_agent.py +147 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Kimss
|
|
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.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kimss-forge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kimss Forge — open-source agent harness: run agents with any model for free; one line to connect Kimss governance when you need production control.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://kimss.ai/open-source
|
|
7
|
+
Project-URL: Documentation, https://kimss.ai/docs/agent_harness
|
|
8
|
+
Project-URL: Repository, https://github.com/kimss-ai/kimss-forge
|
|
9
|
+
Project-URL: Issues, https://github.com/kimss-ai/kimss-forge/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/kimss-ai/kimss-forge/blob/main/CHANGELOG.md
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.28
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
25
|
+
Provides-Extra: mcp
|
|
26
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Kimss Forge — open-source agent harness
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
34
|
+
|
|
35
|
+
**Build and run agents with any model — free, forever. Connect Kimss when production needs governance.**
|
|
36
|
+
|
|
37
|
+
**Kimss Forge** (`kimss-forge` on PyPI, `import kimss_forge`) is a small MIT-licensed harness: a local multi-turn tool loop that talks to any OpenAI-compatible endpoint (OpenAI, Azure/Foundry, Anthropic-compatible proxies, Ollama, vLLM). **No Kimss account required** to develop and test.
|
|
38
|
+
|
|
39
|
+
When your security team needs identity, kill switch, budgets, and audit for production, flip one argument — `gateway="kimss"` — and traffic routes through the [Kimss AI Gateway](https://kimss.ai).
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from kimss_forge import Agent, tool
|
|
43
|
+
|
|
44
|
+
@tool
|
|
45
|
+
def multiply(a: float, b: float) -> float:
|
|
46
|
+
"""Multiply two numbers."""
|
|
47
|
+
return a * b
|
|
48
|
+
|
|
49
|
+
# Local — your provider key only
|
|
50
|
+
agent = Agent(model="gpt-4o-mini", tools=[multiply])
|
|
51
|
+
print(agent.run("What is 6 times 7?"))
|
|
52
|
+
|
|
53
|
+
# Production — one line to the Kimss control plane
|
|
54
|
+
agent = Agent(
|
|
55
|
+
model="custom:your-vaulted-model",
|
|
56
|
+
tools=[multiply],
|
|
57
|
+
gateway="kimss",
|
|
58
|
+
agent_id="ops_bot",
|
|
59
|
+
workspace_key="kimss_...",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Naming (unified)
|
|
64
|
+
|
|
65
|
+
| Surface | Name |
|
|
66
|
+
|---------|------|
|
|
67
|
+
| Product | **Kimss Forge** |
|
|
68
|
+
| GitHub | [kimss-ai/kimss-forge](https://github.com/kimss-ai/kimss-forge) |
|
|
69
|
+
| PyPI | `kimss-forge` |
|
|
70
|
+
| Import | `import kimss_forge` / `from kimss_forge import Agent` |
|
|
71
|
+
| Monorepo SSOT | `kimssApi/kimss-forge/` |
|
|
72
|
+
|
|
73
|
+
## Why this exists
|
|
74
|
+
|
|
75
|
+
| Layer | Cost | What you get |
|
|
76
|
+
|-------|------|----------------|
|
|
77
|
+
| **Forge (this package)** | Free / MIT | Run agents, tools, MCP; any model endpoint |
|
|
78
|
+
| **Kimss Developer gateway** | Free tier (25k governed requests/mo) | Identity, audit trail, **kill switch** |
|
|
79
|
+
| **Kimss Production+** | Paid | Authority Boundary, RBAC, Threat Intercepts, PII scrub |
|
|
80
|
+
| **Kimss Scale / Enterprise** | Paid | SSO, SCIM, retention, schema isolation |
|
|
81
|
+
|
|
82
|
+
Developers adopt Forge to ship faster. CISOs purchase the control plane when production governance is mandatory — without rewriting the agent.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install kimss-forge
|
|
88
|
+
# optional MCP tools:
|
|
89
|
+
pip install 'kimss-forge[mcp]'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Local (zero Kimss)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export OPENAI_API_KEY=sk-...
|
|
96
|
+
# optional: OPENAI_BASE_URL=https://your-endpoint/v1
|
|
97
|
+
python examples/01_local_agent.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Production via Kimss (one line)
|
|
101
|
+
|
|
102
|
+
1. Sign up at [kimss.ai](https://kimss.ai/app/signup) (Developer tier is free).
|
|
103
|
+
2. Vault your provider under **Connected Infrastructure**.
|
|
104
|
+
3. Mint a `kimss_...` workspace key.
|
|
105
|
+
4. Set `gateway="kimss"`, `agent_id=...`, and the workspace key.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
Agent(
|
|
109
|
+
model="custom:your-vaulted-model",
|
|
110
|
+
gateway="kimss", # → https://api.kimss.ai/v1
|
|
111
|
+
agent_id="fleet_reporter",
|
|
112
|
+
workspace_key="kimss_...",
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every call sends `X-Kimss-Agent-Id`. Disable that agent in the Kimss UI and subsequent hops return **403** (kill switch). Paid tiers add Authority Boundary, Team & Access, SCIM/SSO, and more — see [pricing](https://kimss.ai/pricing).
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
| Script | What it shows |
|
|
121
|
+
|--------|----------------|
|
|
122
|
+
| `examples/01_local_agent.py` | Local chat, no tools |
|
|
123
|
+
| `examples/02_tools.py` | `@tool` functions |
|
|
124
|
+
| `examples/03_mcp.py` | MCP stdio tools (`[mcp]` extra) |
|
|
125
|
+
| `examples/04_production_kimss.py` | Gateway connect |
|
|
126
|
+
|
|
127
|
+
## Design decision
|
|
128
|
+
|
|
129
|
+
See [ADR.md](ADR.md) (why Forge is separate from the proprietary Hermis server loop, and why kill switch stays free).
|
|
130
|
+
|
|
131
|
+
## Related
|
|
132
|
+
|
|
133
|
+
- Control-plane client (register agents, report usage): [`kimss`](https://pypi.org/project/kimss/) / [kimss-python-sdk](https://github.com/kimss-ai/kimss-python-sdk)
|
|
134
|
+
- Gateway quickstart: [kimss-python-quickstart](https://github.com/kimss-ai/kimss-python-quickstart)
|
|
135
|
+
- Docs: [Agent harness](https://kimss.ai/docs/agent_harness) · [Open source](https://kimss.ai/open-source)
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Kimss Forge — open-source agent harness
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
6
|
+
|
|
7
|
+
**Build and run agents with any model — free, forever. Connect Kimss when production needs governance.**
|
|
8
|
+
|
|
9
|
+
**Kimss Forge** (`kimss-forge` on PyPI, `import kimss_forge`) is a small MIT-licensed harness: a local multi-turn tool loop that talks to any OpenAI-compatible endpoint (OpenAI, Azure/Foundry, Anthropic-compatible proxies, Ollama, vLLM). **No Kimss account required** to develop and test.
|
|
10
|
+
|
|
11
|
+
When your security team needs identity, kill switch, budgets, and audit for production, flip one argument — `gateway="kimss"` — and traffic routes through the [Kimss AI Gateway](https://kimss.ai).
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from kimss_forge import Agent, tool
|
|
15
|
+
|
|
16
|
+
@tool
|
|
17
|
+
def multiply(a: float, b: float) -> float:
|
|
18
|
+
"""Multiply two numbers."""
|
|
19
|
+
return a * b
|
|
20
|
+
|
|
21
|
+
# Local — your provider key only
|
|
22
|
+
agent = Agent(model="gpt-4o-mini", tools=[multiply])
|
|
23
|
+
print(agent.run("What is 6 times 7?"))
|
|
24
|
+
|
|
25
|
+
# Production — one line to the Kimss control plane
|
|
26
|
+
agent = Agent(
|
|
27
|
+
model="custom:your-vaulted-model",
|
|
28
|
+
tools=[multiply],
|
|
29
|
+
gateway="kimss",
|
|
30
|
+
agent_id="ops_bot",
|
|
31
|
+
workspace_key="kimss_...",
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Naming (unified)
|
|
36
|
+
|
|
37
|
+
| Surface | Name |
|
|
38
|
+
|---------|------|
|
|
39
|
+
| Product | **Kimss Forge** |
|
|
40
|
+
| GitHub | [kimss-ai/kimss-forge](https://github.com/kimss-ai/kimss-forge) |
|
|
41
|
+
| PyPI | `kimss-forge` |
|
|
42
|
+
| Import | `import kimss_forge` / `from kimss_forge import Agent` |
|
|
43
|
+
| Monorepo SSOT | `kimssApi/kimss-forge/` |
|
|
44
|
+
|
|
45
|
+
## Why this exists
|
|
46
|
+
|
|
47
|
+
| Layer | Cost | What you get |
|
|
48
|
+
|-------|------|----------------|
|
|
49
|
+
| **Forge (this package)** | Free / MIT | Run agents, tools, MCP; any model endpoint |
|
|
50
|
+
| **Kimss Developer gateway** | Free tier (25k governed requests/mo) | Identity, audit trail, **kill switch** |
|
|
51
|
+
| **Kimss Production+** | Paid | Authority Boundary, RBAC, Threat Intercepts, PII scrub |
|
|
52
|
+
| **Kimss Scale / Enterprise** | Paid | SSO, SCIM, retention, schema isolation |
|
|
53
|
+
|
|
54
|
+
Developers adopt Forge to ship faster. CISOs purchase the control plane when production governance is mandatory — without rewriting the agent.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install kimss-forge
|
|
60
|
+
# optional MCP tools:
|
|
61
|
+
pip install 'kimss-forge[mcp]'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Local (zero Kimss)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export OPENAI_API_KEY=sk-...
|
|
68
|
+
# optional: OPENAI_BASE_URL=https://your-endpoint/v1
|
|
69
|
+
python examples/01_local_agent.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Production via Kimss (one line)
|
|
73
|
+
|
|
74
|
+
1. Sign up at [kimss.ai](https://kimss.ai/app/signup) (Developer tier is free).
|
|
75
|
+
2. Vault your provider under **Connected Infrastructure**.
|
|
76
|
+
3. Mint a `kimss_...` workspace key.
|
|
77
|
+
4. Set `gateway="kimss"`, `agent_id=...`, and the workspace key.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
Agent(
|
|
81
|
+
model="custom:your-vaulted-model",
|
|
82
|
+
gateway="kimss", # → https://api.kimss.ai/v1
|
|
83
|
+
agent_id="fleet_reporter",
|
|
84
|
+
workspace_key="kimss_...",
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every call sends `X-Kimss-Agent-Id`. Disable that agent in the Kimss UI and subsequent hops return **403** (kill switch). Paid tiers add Authority Boundary, Team & Access, SCIM/SSO, and more — see [pricing](https://kimss.ai/pricing).
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
| Script | What it shows |
|
|
93
|
+
|--------|----------------|
|
|
94
|
+
| `examples/01_local_agent.py` | Local chat, no tools |
|
|
95
|
+
| `examples/02_tools.py` | `@tool` functions |
|
|
96
|
+
| `examples/03_mcp.py` | MCP stdio tools (`[mcp]` extra) |
|
|
97
|
+
| `examples/04_production_kimss.py` | Gateway connect |
|
|
98
|
+
|
|
99
|
+
## Design decision
|
|
100
|
+
|
|
101
|
+
See [ADR.md](ADR.md) (why Forge is separate from the proprietary Hermis server loop, and why kill switch stays free).
|
|
102
|
+
|
|
103
|
+
## Related
|
|
104
|
+
|
|
105
|
+
- Control-plane client (register agents, report usage): [`kimss`](https://pypi.org/project/kimss/) / [kimss-python-sdk](https://github.com/kimss-ai/kimss-python-sdk)
|
|
106
|
+
- Gateway quickstart: [kimss-python-quickstart](https://github.com/kimss-ai/kimss-python-quickstart)
|
|
107
|
+
- Docs: [Agent harness](https://kimss.ai/docs/agent_harness) · [Open source](https://kimss.ai/open-source)
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""kimss_forge — Kimss Forge open-source agent harness (MIT).
|
|
2
|
+
|
|
3
|
+
Run agents against any OpenAI-compatible endpoint with zero Kimss account.
|
|
4
|
+
Connect the Kimss AI Gateway with one line when you need production governance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .agent import Agent, AgentResult
|
|
8
|
+
from .gateway import KIMSS_GATEWAY_BASE_URL, gateway_headers
|
|
9
|
+
from .tools import Tool, tool
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Agent",
|
|
14
|
+
"AgentResult",
|
|
15
|
+
"Tool",
|
|
16
|
+
"tool",
|
|
17
|
+
"gateway_headers",
|
|
18
|
+
"KIMSS_GATEWAY_BASE_URL",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""High-level Agent API — local by default, Kimss gateway with one line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
7
|
+
|
|
8
|
+
from .client import ChatClient
|
|
9
|
+
from .gateway import apply_kimss_gateway
|
|
10
|
+
from .loop import AgentResult, run_loop
|
|
11
|
+
from .tools import Tool, coerce_tools
|
|
12
|
+
|
|
13
|
+
ToolLike = Union[Tool, Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Agent:
|
|
17
|
+
"""
|
|
18
|
+
Standalone agent harness.
|
|
19
|
+
|
|
20
|
+
**Local (free, no Kimss account)::**
|
|
21
|
+
|
|
22
|
+
agent = Agent(model="gpt-4o-mini", instructions="Be brief.", tools=[...])
|
|
23
|
+
print(agent.run("Hello"))
|
|
24
|
+
|
|
25
|
+
**Production via Kimss gateway (one line)::**
|
|
26
|
+
|
|
27
|
+
agent = Agent(
|
|
28
|
+
model="custom:your-vaulted-model",
|
|
29
|
+
instructions="Be brief.",
|
|
30
|
+
gateway="kimss",
|
|
31
|
+
agent_id="my_agent",
|
|
32
|
+
workspace_key="kimss_...",
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
model: str,
|
|
40
|
+
instructions: str = "You are a helpful assistant.",
|
|
41
|
+
tools: Optional[Sequence[ToolLike]] = None,
|
|
42
|
+
api_key: Optional[str] = None,
|
|
43
|
+
base_url: Optional[str] = None,
|
|
44
|
+
gateway: Optional[str] = None,
|
|
45
|
+
workspace_key: Optional[str] = None,
|
|
46
|
+
agent_id: Optional[str] = None,
|
|
47
|
+
agent_name: Optional[str] = None,
|
|
48
|
+
max_hops: int = 8,
|
|
49
|
+
max_tokens: Optional[int] = None,
|
|
50
|
+
temperature: Optional[float] = None,
|
|
51
|
+
timeout: float = 120.0,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.model = (model or "").strip()
|
|
54
|
+
if not self.model:
|
|
55
|
+
raise ValueError("model is required")
|
|
56
|
+
self.instructions = instructions or ""
|
|
57
|
+
self.tools = coerce_tools(list(tools) if tools else [])
|
|
58
|
+
self.max_hops = max_hops
|
|
59
|
+
self.max_tokens = max_tokens
|
|
60
|
+
self.temperature = temperature
|
|
61
|
+
|
|
62
|
+
gw = (gateway or "").strip().lower()
|
|
63
|
+
extra_headers: Dict[str, str] = {}
|
|
64
|
+
resolved_key = api_key
|
|
65
|
+
resolved_base = base_url
|
|
66
|
+
|
|
67
|
+
if gw in ("kimss", "kimss-ai", "true"):
|
|
68
|
+
resolved_base, resolved_key, extra_headers = apply_kimss_gateway(
|
|
69
|
+
workspace_key=workspace_key or api_key,
|
|
70
|
+
agent_id=agent_id,
|
|
71
|
+
agent_name=agent_name,
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
)
|
|
74
|
+
elif gw:
|
|
75
|
+
raise ValueError(f"Unsupported gateway={gateway!r}; use None or 'kimss'")
|
|
76
|
+
|
|
77
|
+
if resolved_key is None and not gw:
|
|
78
|
+
resolved_key = os.environ.get("OPENAI_API_KEY")
|
|
79
|
+
if resolved_base is None and not gw:
|
|
80
|
+
resolved_base = os.environ.get("OPENAI_BASE_URL")
|
|
81
|
+
|
|
82
|
+
self.agent_id = (agent_id or "").strip() or None
|
|
83
|
+
self._client = ChatClient(
|
|
84
|
+
api_key=resolved_key,
|
|
85
|
+
base_url=resolved_base,
|
|
86
|
+
default_headers=extra_headers,
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def run(
|
|
91
|
+
self,
|
|
92
|
+
user_text: str,
|
|
93
|
+
*,
|
|
94
|
+
messages: Optional[List[Dict[str, Any]]] = None,
|
|
95
|
+
) -> AgentResult:
|
|
96
|
+
"""Run one user turn (multi-hop tool loop). Returns AgentResult (str-compatible)."""
|
|
97
|
+
history: List[Dict[str, Any]] = []
|
|
98
|
+
if self.instructions.strip():
|
|
99
|
+
history.append({"role": "system", "content": self.instructions})
|
|
100
|
+
if messages:
|
|
101
|
+
history.extend(messages)
|
|
102
|
+
history.append({"role": "user", "content": user_text})
|
|
103
|
+
return run_loop(
|
|
104
|
+
client=self._client,
|
|
105
|
+
model=self.model,
|
|
106
|
+
messages=history,
|
|
107
|
+
tools=self.tools,
|
|
108
|
+
max_hops=self.max_hops,
|
|
109
|
+
max_tokens=self.max_tokens,
|
|
110
|
+
temperature=self.temperature,
|
|
111
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Provider-agnostic OpenAI-compatible chat completions client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompletionError(RuntimeError):
|
|
15
|
+
"""Raised when the model endpoint returns an error."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status_code: int, body: str):
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.body = body
|
|
20
|
+
super().__init__(f"chat completions failed ({status_code}): {body[:400]}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatClient:
|
|
24
|
+
"""Minimal OpenAI-compatible `/chat/completions` client (requests only)."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
api_key: Optional[str] = None,
|
|
30
|
+
base_url: Optional[str] = None,
|
|
31
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
32
|
+
timeout: float = 120.0,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.api_key = (api_key or os.environ.get("OPENAI_API_KEY") or "").strip()
|
|
35
|
+
raw_base = (
|
|
36
|
+
base_url
|
|
37
|
+
or os.environ.get("OPENAI_BASE_URL")
|
|
38
|
+
or DEFAULT_OPENAI_BASE_URL
|
|
39
|
+
).rstrip("/")
|
|
40
|
+
self.base_url = raw_base
|
|
41
|
+
self.default_headers = dict(default_headers or {})
|
|
42
|
+
self.timeout = timeout
|
|
43
|
+
|
|
44
|
+
def chat_completions(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
model: str,
|
|
48
|
+
messages: List[Dict[str, Any]],
|
|
49
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
50
|
+
max_tokens: Optional[int] = None,
|
|
51
|
+
temperature: Optional[float] = None,
|
|
52
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
53
|
+
) -> Dict[str, Any]:
|
|
54
|
+
if not self.api_key:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"API key required: pass api_key=... or set OPENAI_API_KEY "
|
|
57
|
+
"(or use gateway='kimss' with a Kimss workspace key)"
|
|
58
|
+
)
|
|
59
|
+
url = f"{self.base_url}/chat/completions"
|
|
60
|
+
payload: Dict[str, Any] = {"model": model, "messages": messages}
|
|
61
|
+
if tools:
|
|
62
|
+
payload["tools"] = tools
|
|
63
|
+
payload["tool_choice"] = "auto"
|
|
64
|
+
if max_tokens is not None:
|
|
65
|
+
payload["max_tokens"] = max_tokens
|
|
66
|
+
if temperature is not None:
|
|
67
|
+
payload["temperature"] = temperature
|
|
68
|
+
|
|
69
|
+
headers = {
|
|
70
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
**self.default_headers,
|
|
73
|
+
**(extra_headers or {}),
|
|
74
|
+
}
|
|
75
|
+
resp = requests.post(url, headers=headers, json=payload, timeout=self.timeout)
|
|
76
|
+
if resp.status_code >= 400:
|
|
77
|
+
raise CompletionError(resp.status_code, resp.text)
|
|
78
|
+
try:
|
|
79
|
+
return resp.json()
|
|
80
|
+
except json.JSONDecodeError as exc:
|
|
81
|
+
raise CompletionError(resp.status_code, resp.text) from exc
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Kimss AI Gateway connect helpers (one-line production upgrade)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
KIMSS_GATEWAY_BASE_URL = "https://api.kimss.ai/v1"
|
|
9
|
+
KIMSS_GATEWAY_HOST = "https://api.kimss.ai"
|
|
10
|
+
|
|
11
|
+
# Header names match kimss_sdk / Invisible Proxy conventions.
|
|
12
|
+
HEADER_AGENT_ID = "X-Kimss-Agent-Id"
|
|
13
|
+
HEADER_AGENT_NAME = "X-Kimss-Agent-Name"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def gateway_headers(
|
|
17
|
+
*,
|
|
18
|
+
agent_id: str,
|
|
19
|
+
agent_name: Optional[str] = None,
|
|
20
|
+
) -> Dict[str, str]:
|
|
21
|
+
"""Return attribution headers for traffic routed through the Kimss gateway."""
|
|
22
|
+
aid = (agent_id or "").strip()
|
|
23
|
+
if not aid:
|
|
24
|
+
raise ValueError("agent_id is required when using the Kimss gateway")
|
|
25
|
+
headers: Dict[str, str] = {HEADER_AGENT_ID: aid}
|
|
26
|
+
name = (agent_name or "").strip()
|
|
27
|
+
if name:
|
|
28
|
+
headers[HEADER_AGENT_NAME] = name
|
|
29
|
+
return headers
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_kimss_workspace_key(explicit: Optional[str] = None) -> str:
|
|
33
|
+
"""Resolve workspace key from argument or env (KIMSS_API_KEY / KIMSS_WORKSPACE_KEY)."""
|
|
34
|
+
key = (explicit or "").strip()
|
|
35
|
+
if key:
|
|
36
|
+
return key
|
|
37
|
+
for env_name in ("KIMSS_API_KEY", "KIMSS_WORKSPACE_KEY"):
|
|
38
|
+
val = (os.environ.get(env_name) or "").strip()
|
|
39
|
+
if val:
|
|
40
|
+
return val
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"Kimss workspace key required: pass workspace_key=... or set "
|
|
43
|
+
"KIMSS_API_KEY / KIMSS_WORKSPACE_KEY"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_kimss_gateway(
|
|
48
|
+
*,
|
|
49
|
+
workspace_key: Optional[str] = None,
|
|
50
|
+
agent_id: Optional[str] = None,
|
|
51
|
+
agent_name: Optional[str] = None,
|
|
52
|
+
base_url: Optional[str] = None,
|
|
53
|
+
) -> tuple[str, str, Dict[str, str]]:
|
|
54
|
+
"""
|
|
55
|
+
Resolve base_url, api_key, and extra headers for Kimss gateway mode.
|
|
56
|
+
|
|
57
|
+
Returns (base_url, api_key, extra_headers).
|
|
58
|
+
"""
|
|
59
|
+
key = resolve_kimss_workspace_key(workspace_key)
|
|
60
|
+
aid = (agent_id or os.environ.get("KIMSS_AGENT_ID") or "").strip()
|
|
61
|
+
if not aid:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"agent_id is required for gateway='kimss' "
|
|
64
|
+
"(pass agent_id=... or set KIMSS_AGENT_ID)"
|
|
65
|
+
)
|
|
66
|
+
url = (base_url or KIMSS_GATEWAY_BASE_URL).rstrip("/")
|
|
67
|
+
headers = gateway_headers(agent_id=aid, agent_name=agent_name)
|
|
68
|
+
return url, key, headers
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Local multi-turn agent loop (model → tools → model). No Kimss governance logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .client import ChatClient
|
|
10
|
+
from .tools import Tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAX_HOPS = 8
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AgentResult:
|
|
18
|
+
"""Outcome of a single ``Agent.run`` call."""
|
|
19
|
+
|
|
20
|
+
text: str
|
|
21
|
+
messages: List[Dict[str, Any]] = field(default_factory=list)
|
|
22
|
+
hops: int = 0
|
|
23
|
+
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
|
24
|
+
raw: Optional[Dict[str, Any]] = None
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return self.text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _assistant_message_from_choice(choice: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
msg = choice.get("message") or {}
|
|
32
|
+
out: Dict[str, Any] = {"role": "assistant"}
|
|
33
|
+
content = msg.get("content")
|
|
34
|
+
if content is not None:
|
|
35
|
+
out["content"] = content
|
|
36
|
+
tool_calls = msg.get("tool_calls")
|
|
37
|
+
if tool_calls:
|
|
38
|
+
out["tool_calls"] = tool_calls
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _stringify_content(content: Any) -> str:
|
|
43
|
+
if content is None:
|
|
44
|
+
return ""
|
|
45
|
+
if isinstance(content, str):
|
|
46
|
+
return content
|
|
47
|
+
try:
|
|
48
|
+
return json.dumps(content, default=str)
|
|
49
|
+
except TypeError:
|
|
50
|
+
return str(content)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_loop(
|
|
54
|
+
*,
|
|
55
|
+
client: ChatClient,
|
|
56
|
+
model: str,
|
|
57
|
+
messages: List[Dict[str, Any]],
|
|
58
|
+
tools: List[Tool],
|
|
59
|
+
max_hops: int = DEFAULT_MAX_HOPS,
|
|
60
|
+
max_tokens: Optional[int] = None,
|
|
61
|
+
temperature: Optional[float] = None,
|
|
62
|
+
) -> AgentResult:
|
|
63
|
+
"""
|
|
64
|
+
Run a multi-turn tool loop until the model returns text or max_hops is hit.
|
|
65
|
+
|
|
66
|
+
Pure local orchestration — no Authority Boundary, vault, or plan checks.
|
|
67
|
+
"""
|
|
68
|
+
tool_map = {t.name: t for t in tools}
|
|
69
|
+
openai_tools = [t.openai_schema() for t in tools] or None
|
|
70
|
+
history = list(messages)
|
|
71
|
+
recorded_calls: List[Dict[str, Any]] = []
|
|
72
|
+
last_raw: Optional[Dict[str, Any]] = None
|
|
73
|
+
hops = 0
|
|
74
|
+
|
|
75
|
+
for hop in range(max(1, max_hops)):
|
|
76
|
+
hops = hop + 1
|
|
77
|
+
raw = client.chat_completions(
|
|
78
|
+
model=model,
|
|
79
|
+
messages=history,
|
|
80
|
+
tools=openai_tools,
|
|
81
|
+
max_tokens=max_tokens,
|
|
82
|
+
temperature=temperature,
|
|
83
|
+
)
|
|
84
|
+
last_raw = raw
|
|
85
|
+
choices = raw.get("choices") or []
|
|
86
|
+
if not choices:
|
|
87
|
+
return AgentResult(
|
|
88
|
+
text="",
|
|
89
|
+
messages=history,
|
|
90
|
+
hops=hops,
|
|
91
|
+
tool_calls=recorded_calls,
|
|
92
|
+
raw=raw,
|
|
93
|
+
)
|
|
94
|
+
assistant = _assistant_message_from_choice(choices[0])
|
|
95
|
+
history.append(assistant)
|
|
96
|
+
tool_calls = assistant.get("tool_calls") or []
|
|
97
|
+
if not tool_calls:
|
|
98
|
+
return AgentResult(
|
|
99
|
+
text=_stringify_content(assistant.get("content")),
|
|
100
|
+
messages=history,
|
|
101
|
+
hops=hops,
|
|
102
|
+
tool_calls=recorded_calls,
|
|
103
|
+
raw=raw,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
for tc in tool_calls:
|
|
107
|
+
fn = (tc.get("function") or {}) if isinstance(tc, dict) else {}
|
|
108
|
+
name = str(fn.get("name") or "").strip()
|
|
109
|
+
args_raw = fn.get("arguments")
|
|
110
|
+
call_id = str(tc.get("id") or name or "tool")
|
|
111
|
+
recorded_calls.append({"id": call_id, "name": name, "arguments": args_raw})
|
|
112
|
+
if name not in tool_map:
|
|
113
|
+
result_text = json.dumps({"error": f"unknown tool: {name}"})
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
result_text = tool_map[name].call(args_raw)
|
|
117
|
+
except Exception as exc: # noqa: BLE001 — surface tool errors to the model
|
|
118
|
+
result_text = json.dumps({"error": str(exc)})
|
|
119
|
+
history.append(
|
|
120
|
+
{
|
|
121
|
+
"role": "tool",
|
|
122
|
+
"tool_call_id": call_id,
|
|
123
|
+
"content": result_text,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Max hops exhausted after tool rounds — ask once more without tools for a final answer.
|
|
128
|
+
raw = client.chat_completions(
|
|
129
|
+
model=model,
|
|
130
|
+
messages=history,
|
|
131
|
+
tools=None,
|
|
132
|
+
max_tokens=max_tokens,
|
|
133
|
+
temperature=temperature,
|
|
134
|
+
)
|
|
135
|
+
last_raw = raw
|
|
136
|
+
choices = raw.get("choices") or []
|
|
137
|
+
text = ""
|
|
138
|
+
if choices:
|
|
139
|
+
assistant = _assistant_message_from_choice(choices[0])
|
|
140
|
+
history.append(assistant)
|
|
141
|
+
text = _stringify_content(assistant.get("content"))
|
|
142
|
+
return AgentResult(
|
|
143
|
+
text=text,
|
|
144
|
+
messages=history,
|
|
145
|
+
hops=hops,
|
|
146
|
+
tool_calls=recorded_calls,
|
|
147
|
+
raw=last_raw,
|
|
148
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Optional MCP tool loading (install with ``pip install 'kimss-forge[mcp]'``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .tools import Tool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mcp_tools_from_stdio(
|
|
13
|
+
command: str,
|
|
14
|
+
args: Optional[List[str]] = None,
|
|
15
|
+
*,
|
|
16
|
+
env: Optional[Dict[str, str]] = None,
|
|
17
|
+
) -> List[Tool]:
|
|
18
|
+
"""
|
|
19
|
+
Connect to an MCP server over stdio and wrap its tools as harness Tools.
|
|
20
|
+
|
|
21
|
+
Requires the optional ``mcp`` extra. Tools execute via a fresh short-lived
|
|
22
|
+
session per call (simple; suitable for demos and scripts).
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
from mcp import ClientSession, StdioServerParameters
|
|
26
|
+
from mcp.client.stdio import stdio_client
|
|
27
|
+
except ImportError as exc:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"MCP support requires: pip install 'kimss-forge[mcp]'"
|
|
30
|
+
) from exc
|
|
31
|
+
|
|
32
|
+
server = StdioServerParameters(command=command, args=args or [], env=env)
|
|
33
|
+
|
|
34
|
+
async def _list() -> List[Dict[str, Any]]:
|
|
35
|
+
async with stdio_client(server) as (read, write):
|
|
36
|
+
async with ClientSession(read, write) as session:
|
|
37
|
+
await session.initialize()
|
|
38
|
+
listed = await session.list_tools()
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"name": t.name,
|
|
42
|
+
"description": (t.description or t.name),
|
|
43
|
+
"inputSchema": getattr(t, "inputSchema", None) or {},
|
|
44
|
+
}
|
|
45
|
+
for t in (listed.tools or [])
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
specs = asyncio.run(_list())
|
|
49
|
+
out: List[Tool] = []
|
|
50
|
+
|
|
51
|
+
for spec in specs:
|
|
52
|
+
name = str(spec["name"])
|
|
53
|
+
description = str(spec["description"])
|
|
54
|
+
parameters = spec.get("inputSchema") or {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def _make_fn(tool_name: str):
|
|
60
|
+
def _fn(**kwargs: Any) -> str:
|
|
61
|
+
async def _call() -> str:
|
|
62
|
+
async with stdio_client(server) as (read, write):
|
|
63
|
+
async with ClientSession(read, write) as session:
|
|
64
|
+
await session.initialize()
|
|
65
|
+
result = await session.call_tool(tool_name, arguments=kwargs)
|
|
66
|
+
parts = []
|
|
67
|
+
for block in getattr(result, "content", None) or []:
|
|
68
|
+
text = getattr(block, "text", None)
|
|
69
|
+
if text is not None:
|
|
70
|
+
parts.append(text)
|
|
71
|
+
else:
|
|
72
|
+
parts.append(str(block))
|
|
73
|
+
return "\n".join(parts) if parts else json.dumps(
|
|
74
|
+
{"ok": True}, default=str
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return asyncio.run(_call())
|
|
78
|
+
|
|
79
|
+
_fn.__name__ = tool_name
|
|
80
|
+
_fn.__doc__ = description
|
|
81
|
+
return _fn
|
|
82
|
+
|
|
83
|
+
out.append(
|
|
84
|
+
Tool(
|
|
85
|
+
name=name,
|
|
86
|
+
description=description,
|
|
87
|
+
parameters=parameters if isinstance(parameters, dict) else {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {},
|
|
90
|
+
},
|
|
91
|
+
fn=_make_fn(name),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
return out
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Tool registration for the local agent loop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, get_args, get_origin, get_type_hints
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _json_type(annotation: Any) -> str:
|
|
12
|
+
origin = get_origin(annotation)
|
|
13
|
+
if origin is list or annotation is list:
|
|
14
|
+
return "array"
|
|
15
|
+
if origin is dict or annotation is dict:
|
|
16
|
+
return "object"
|
|
17
|
+
if annotation is bool:
|
|
18
|
+
return "boolean"
|
|
19
|
+
if annotation is int:
|
|
20
|
+
return "integer"
|
|
21
|
+
if annotation is float:
|
|
22
|
+
return "number"
|
|
23
|
+
if annotation is str or annotation is Any or annotation is inspect.Parameter.empty:
|
|
24
|
+
return "string"
|
|
25
|
+
# Optional[T] / Union
|
|
26
|
+
args = get_args(annotation)
|
|
27
|
+
if args:
|
|
28
|
+
non_none = [a for a in args if a is not type(None)]
|
|
29
|
+
if len(non_none) == 1:
|
|
30
|
+
return _json_type(non_none[0])
|
|
31
|
+
return "string"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Tool:
|
|
36
|
+
"""A callable tool with an OpenAI-compatible function schema."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
description: str
|
|
40
|
+
parameters: Dict[str, Any]
|
|
41
|
+
fn: Callable[..., Any]
|
|
42
|
+
|
|
43
|
+
def openai_schema(self) -> Dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"type": "function",
|
|
46
|
+
"function": {
|
|
47
|
+
"name": self.name,
|
|
48
|
+
"description": self.description,
|
|
49
|
+
"parameters": self.parameters,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def call(self, arguments: Dict[str, Any] | str | None) -> str:
|
|
54
|
+
if arguments is None:
|
|
55
|
+
args: Dict[str, Any] = {}
|
|
56
|
+
elif isinstance(arguments, str):
|
|
57
|
+
try:
|
|
58
|
+
args = json.loads(arguments) if arguments.strip() else {}
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
args = {"raw": arguments}
|
|
61
|
+
else:
|
|
62
|
+
args = dict(arguments)
|
|
63
|
+
result = self.fn(**args) if args else self.fn()
|
|
64
|
+
if result is None:
|
|
65
|
+
return ""
|
|
66
|
+
if isinstance(result, str):
|
|
67
|
+
return result
|
|
68
|
+
try:
|
|
69
|
+
return json.dumps(result, default=str)
|
|
70
|
+
except TypeError:
|
|
71
|
+
return str(result)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def tool(
|
|
75
|
+
fn: Optional[Callable[..., Any]] = None,
|
|
76
|
+
*,
|
|
77
|
+
name: Optional[str] = None,
|
|
78
|
+
description: Optional[str] = None,
|
|
79
|
+
) -> Any:
|
|
80
|
+
"""Decorator (or wrapper) that turns a Python function into a Tool."""
|
|
81
|
+
|
|
82
|
+
def _wrap(f: Callable[..., Any]) -> Tool:
|
|
83
|
+
hints = {}
|
|
84
|
+
try:
|
|
85
|
+
hints = get_type_hints(f)
|
|
86
|
+
except Exception:
|
|
87
|
+
hints = getattr(f, "__annotations__", {}) or {}
|
|
88
|
+
sig = inspect.signature(f)
|
|
89
|
+
properties: Dict[str, Any] = {}
|
|
90
|
+
required: List[str] = []
|
|
91
|
+
for pname, param in sig.parameters.items():
|
|
92
|
+
if pname in ("self", "cls"):
|
|
93
|
+
continue
|
|
94
|
+
ann = hints.get(pname, param.annotation)
|
|
95
|
+
properties[pname] = {
|
|
96
|
+
"type": _json_type(ann),
|
|
97
|
+
"description": pname.replace("_", " "),
|
|
98
|
+
}
|
|
99
|
+
if param.default is inspect.Parameter.empty:
|
|
100
|
+
required.append(pname)
|
|
101
|
+
schema = {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": properties,
|
|
104
|
+
"additionalProperties": False,
|
|
105
|
+
}
|
|
106
|
+
if required:
|
|
107
|
+
schema["required"] = required
|
|
108
|
+
doc = (description or inspect.getdoc(f) or f.__name__).strip()
|
|
109
|
+
return Tool(
|
|
110
|
+
name=(name or f.__name__).strip(),
|
|
111
|
+
description=doc.split("\n")[0].strip(),
|
|
112
|
+
parameters=schema,
|
|
113
|
+
fn=f,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if fn is not None:
|
|
117
|
+
return _wrap(fn)
|
|
118
|
+
return _wrap
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def coerce_tools(tools: Optional[List[Any]]) -> List[Tool]:
|
|
122
|
+
"""Normalize a list of Tool instances or @tool-decorated callables."""
|
|
123
|
+
out: List[Tool] = []
|
|
124
|
+
for t in tools or []:
|
|
125
|
+
if isinstance(t, Tool):
|
|
126
|
+
out.append(t)
|
|
127
|
+
elif callable(t):
|
|
128
|
+
out.append(tool(t))
|
|
129
|
+
else:
|
|
130
|
+
raise TypeError(f"Unsupported tool type: {type(t)!r}")
|
|
131
|
+
return out
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kimss-forge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kimss Forge — open-source agent harness: run agents with any model for free; one line to connect Kimss governance when you need production control.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://kimss.ai/open-source
|
|
7
|
+
Project-URL: Documentation, https://kimss.ai/docs/agent_harness
|
|
8
|
+
Project-URL: Repository, https://github.com/kimss-ai/kimss-forge
|
|
9
|
+
Project-URL: Issues, https://github.com/kimss-ai/kimss-forge/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/kimss-ai/kimss-forge/blob/main/CHANGELOG.md
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.28
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
25
|
+
Provides-Extra: mcp
|
|
26
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Kimss Forge — open-source agent harness
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://pypi.org/project/kimss-forge/)
|
|
34
|
+
|
|
35
|
+
**Build and run agents with any model — free, forever. Connect Kimss when production needs governance.**
|
|
36
|
+
|
|
37
|
+
**Kimss Forge** (`kimss-forge` on PyPI, `import kimss_forge`) is a small MIT-licensed harness: a local multi-turn tool loop that talks to any OpenAI-compatible endpoint (OpenAI, Azure/Foundry, Anthropic-compatible proxies, Ollama, vLLM). **No Kimss account required** to develop and test.
|
|
38
|
+
|
|
39
|
+
When your security team needs identity, kill switch, budgets, and audit for production, flip one argument — `gateway="kimss"` — and traffic routes through the [Kimss AI Gateway](https://kimss.ai).
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from kimss_forge import Agent, tool
|
|
43
|
+
|
|
44
|
+
@tool
|
|
45
|
+
def multiply(a: float, b: float) -> float:
|
|
46
|
+
"""Multiply two numbers."""
|
|
47
|
+
return a * b
|
|
48
|
+
|
|
49
|
+
# Local — your provider key only
|
|
50
|
+
agent = Agent(model="gpt-4o-mini", tools=[multiply])
|
|
51
|
+
print(agent.run("What is 6 times 7?"))
|
|
52
|
+
|
|
53
|
+
# Production — one line to the Kimss control plane
|
|
54
|
+
agent = Agent(
|
|
55
|
+
model="custom:your-vaulted-model",
|
|
56
|
+
tools=[multiply],
|
|
57
|
+
gateway="kimss",
|
|
58
|
+
agent_id="ops_bot",
|
|
59
|
+
workspace_key="kimss_...",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Naming (unified)
|
|
64
|
+
|
|
65
|
+
| Surface | Name |
|
|
66
|
+
|---------|------|
|
|
67
|
+
| Product | **Kimss Forge** |
|
|
68
|
+
| GitHub | [kimss-ai/kimss-forge](https://github.com/kimss-ai/kimss-forge) |
|
|
69
|
+
| PyPI | `kimss-forge` |
|
|
70
|
+
| Import | `import kimss_forge` / `from kimss_forge import Agent` |
|
|
71
|
+
| Monorepo SSOT | `kimssApi/kimss-forge/` |
|
|
72
|
+
|
|
73
|
+
## Why this exists
|
|
74
|
+
|
|
75
|
+
| Layer | Cost | What you get |
|
|
76
|
+
|-------|------|----------------|
|
|
77
|
+
| **Forge (this package)** | Free / MIT | Run agents, tools, MCP; any model endpoint |
|
|
78
|
+
| **Kimss Developer gateway** | Free tier (25k governed requests/mo) | Identity, audit trail, **kill switch** |
|
|
79
|
+
| **Kimss Production+** | Paid | Authority Boundary, RBAC, Threat Intercepts, PII scrub |
|
|
80
|
+
| **Kimss Scale / Enterprise** | Paid | SSO, SCIM, retention, schema isolation |
|
|
81
|
+
|
|
82
|
+
Developers adopt Forge to ship faster. CISOs purchase the control plane when production governance is mandatory — without rewriting the agent.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install kimss-forge
|
|
88
|
+
# optional MCP tools:
|
|
89
|
+
pip install 'kimss-forge[mcp]'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Local (zero Kimss)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export OPENAI_API_KEY=sk-...
|
|
96
|
+
# optional: OPENAI_BASE_URL=https://your-endpoint/v1
|
|
97
|
+
python examples/01_local_agent.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Production via Kimss (one line)
|
|
101
|
+
|
|
102
|
+
1. Sign up at [kimss.ai](https://kimss.ai/app/signup) (Developer tier is free).
|
|
103
|
+
2. Vault your provider under **Connected Infrastructure**.
|
|
104
|
+
3. Mint a `kimss_...` workspace key.
|
|
105
|
+
4. Set `gateway="kimss"`, `agent_id=...`, and the workspace key.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
Agent(
|
|
109
|
+
model="custom:your-vaulted-model",
|
|
110
|
+
gateway="kimss", # → https://api.kimss.ai/v1
|
|
111
|
+
agent_id="fleet_reporter",
|
|
112
|
+
workspace_key="kimss_...",
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every call sends `X-Kimss-Agent-Id`. Disable that agent in the Kimss UI and subsequent hops return **403** (kill switch). Paid tiers add Authority Boundary, Team & Access, SCIM/SSO, and more — see [pricing](https://kimss.ai/pricing).
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
| Script | What it shows |
|
|
121
|
+
|--------|----------------|
|
|
122
|
+
| `examples/01_local_agent.py` | Local chat, no tools |
|
|
123
|
+
| `examples/02_tools.py` | `@tool` functions |
|
|
124
|
+
| `examples/03_mcp.py` | MCP stdio tools (`[mcp]` extra) |
|
|
125
|
+
| `examples/04_production_kimss.py` | Gateway connect |
|
|
126
|
+
|
|
127
|
+
## Design decision
|
|
128
|
+
|
|
129
|
+
See [ADR.md](ADR.md) (why Forge is separate from the proprietary Hermis server loop, and why kill switch stays free).
|
|
130
|
+
|
|
131
|
+
## Related
|
|
132
|
+
|
|
133
|
+
- Control-plane client (register agents, report usage): [`kimss`](https://pypi.org/project/kimss/) / [kimss-python-sdk](https://github.com/kimss-ai/kimss-python-sdk)
|
|
134
|
+
- Gateway quickstart: [kimss-python-quickstart](https://github.com/kimss-ai/kimss-python-quickstart)
|
|
135
|
+
- Docs: [Agent harness](https://kimss.ai/docs/agent_harness) · [Open source](https://kimss.ai/open-source)
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
kimss_forge/__init__.py
|
|
5
|
+
kimss_forge/agent.py
|
|
6
|
+
kimss_forge/client.py
|
|
7
|
+
kimss_forge/gateway.py
|
|
8
|
+
kimss_forge/loop.py
|
|
9
|
+
kimss_forge/mcp.py
|
|
10
|
+
kimss_forge/tools.py
|
|
11
|
+
kimss_forge.egg-info/PKG-INFO
|
|
12
|
+
kimss_forge.egg-info/SOURCES.txt
|
|
13
|
+
kimss_forge.egg-info/dependency_links.txt
|
|
14
|
+
kimss_forge.egg-info/requires.txt
|
|
15
|
+
kimss_forge.egg-info/top_level.txt
|
|
16
|
+
tests/test_agent.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kimss_forge
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kimss-forge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Kimss Forge — open-source agent harness: run agents with any model for free; one line to connect Kimss governance when you need production control."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.28",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://kimss.ai/open-source"
|
|
26
|
+
Documentation = "https://kimss.ai/docs/agent_harness"
|
|
27
|
+
Repository = "https://github.com/kimss-ai/kimss-forge"
|
|
28
|
+
Issues = "https://github.com/kimss-ai/kimss-forge/issues"
|
|
29
|
+
Changelog = "https://github.com/kimss-ai/kimss-forge/blob/main/CHANGELOG.md"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7",
|
|
34
|
+
"responses>=0.25",
|
|
35
|
+
"ruff>=0.4",
|
|
36
|
+
]
|
|
37
|
+
mcp = [
|
|
38
|
+
"mcp>=1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["."]
|
|
43
|
+
include = ["kimss_forge*"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 100
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "UP"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Unit tests for kimss-forge (no live network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import responses
|
|
9
|
+
|
|
10
|
+
from kimss_forge import Agent, gateway_headers, tool
|
|
11
|
+
from kimss_forge.gateway import apply_kimss_gateway
|
|
12
|
+
from kimss_forge.tools import coerce_tools
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@tool
|
|
16
|
+
def add(a: int, b: int) -> int:
|
|
17
|
+
"""Add two integers."""
|
|
18
|
+
return a + b
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_tool_schema():
|
|
22
|
+
assert add.name == "add"
|
|
23
|
+
assert "a" in add.parameters["properties"]
|
|
24
|
+
assert add.call({"a": 2, "b": 3}) == "5"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_gateway_headers_require_agent_id():
|
|
28
|
+
with pytest.raises(ValueError):
|
|
29
|
+
gateway_headers(agent_id="")
|
|
30
|
+
h = gateway_headers(agent_id="ops_bot", agent_name="Ops")
|
|
31
|
+
assert h["X-Kimss-Agent-Id"] == "ops_bot"
|
|
32
|
+
assert h["X-Kimss-Agent-Name"] == "Ops"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_apply_kimss_gateway(monkeypatch):
|
|
36
|
+
monkeypatch.delenv("KIMSS_API_KEY", raising=False)
|
|
37
|
+
monkeypatch.delenv("KIMSS_WORKSPACE_KEY", raising=False)
|
|
38
|
+
url, key, headers = apply_kimss_gateway(
|
|
39
|
+
workspace_key="kimss_test",
|
|
40
|
+
agent_id="agent_1",
|
|
41
|
+
)
|
|
42
|
+
assert url.endswith("/v1")
|
|
43
|
+
assert key == "kimss_test"
|
|
44
|
+
assert headers["X-Kimss-Agent-Id"] == "agent_1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@responses.activate
|
|
48
|
+
def test_agent_run_text_only():
|
|
49
|
+
responses.add(
|
|
50
|
+
responses.POST,
|
|
51
|
+
"https://api.openai.com/v1/chat/completions",
|
|
52
|
+
json={
|
|
53
|
+
"choices": [
|
|
54
|
+
{"message": {"role": "assistant", "content": "hello there"}}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
status=200,
|
|
58
|
+
)
|
|
59
|
+
agent = Agent(
|
|
60
|
+
model="gpt-4o-mini",
|
|
61
|
+
instructions="Be brief.",
|
|
62
|
+
api_key="sk-test",
|
|
63
|
+
base_url="https://api.openai.com/v1",
|
|
64
|
+
)
|
|
65
|
+
result = agent.run("Hi")
|
|
66
|
+
assert str(result) == "hello there"
|
|
67
|
+
assert result.hops == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_agent_tool_loop():
|
|
72
|
+
# First response: tool call
|
|
73
|
+
responses.add(
|
|
74
|
+
responses.POST,
|
|
75
|
+
"https://api.openai.com/v1/chat/completions",
|
|
76
|
+
json={
|
|
77
|
+
"choices": [
|
|
78
|
+
{
|
|
79
|
+
"message": {
|
|
80
|
+
"role": "assistant",
|
|
81
|
+
"content": None,
|
|
82
|
+
"tool_calls": [
|
|
83
|
+
{
|
|
84
|
+
"id": "call_1",
|
|
85
|
+
"type": "function",
|
|
86
|
+
"function": {
|
|
87
|
+
"name": "add",
|
|
88
|
+
"arguments": json.dumps({"a": 2, "b": 40}),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
status=200,
|
|
97
|
+
)
|
|
98
|
+
# Second response: final text
|
|
99
|
+
responses.add(
|
|
100
|
+
responses.POST,
|
|
101
|
+
"https://api.openai.com/v1/chat/completions",
|
|
102
|
+
json={
|
|
103
|
+
"choices": [
|
|
104
|
+
{"message": {"role": "assistant", "content": "The sum is 42"}}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
status=200,
|
|
108
|
+
)
|
|
109
|
+
agent = Agent(
|
|
110
|
+
model="gpt-4o-mini",
|
|
111
|
+
tools=[add],
|
|
112
|
+
api_key="sk-test",
|
|
113
|
+
base_url="https://api.openai.com/v1",
|
|
114
|
+
)
|
|
115
|
+
result = agent.run("What is 2+40?")
|
|
116
|
+
assert "42" in result.text
|
|
117
|
+
assert result.tool_calls[0]["name"] == "add"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@responses.activate
|
|
121
|
+
def test_agent_kimss_gateway_headers():
|
|
122
|
+
def _check(request):
|
|
123
|
+
assert request.headers.get("X-Kimss-Agent-Id") == "fleet_bot"
|
|
124
|
+
assert request.headers.get("Authorization") == "Bearer kimss_abc"
|
|
125
|
+
body = {
|
|
126
|
+
"choices": [{"message": {"role": "assistant", "content": "ok"}}]
|
|
127
|
+
}
|
|
128
|
+
return (200, {"Content-Type": "application/json"}, json.dumps(body))
|
|
129
|
+
|
|
130
|
+
responses.add_callback(
|
|
131
|
+
responses.POST,
|
|
132
|
+
"https://api.kimss.ai/v1/chat/completions",
|
|
133
|
+
callback=_check,
|
|
134
|
+
)
|
|
135
|
+
agent = Agent(
|
|
136
|
+
model="custom:demo",
|
|
137
|
+
gateway="kimss",
|
|
138
|
+
workspace_key="kimss_abc",
|
|
139
|
+
agent_id="fleet_bot",
|
|
140
|
+
)
|
|
141
|
+
assert str(agent.run("ping")) == "ok"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_coerce_tools():
|
|
145
|
+
tools = coerce_tools([add])
|
|
146
|
+
assert len(tools) == 1
|
|
147
|
+
assert tools[0].name == "add"
|