base-agentkit 0.1.0__py3-none-any.whl
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.
- agentkit/__init__.py +35 -0
- agentkit/agent/__init__.py +7 -0
- agentkit/agent/agent.py +368 -0
- agentkit/agent/budgets.py +48 -0
- agentkit/agent/report.py +166 -0
- agentkit/agent/tool_runtime.py +77 -0
- agentkit/cli/__init__.py +5 -0
- agentkit/cli/main.py +108 -0
- agentkit/config/__init__.py +23 -0
- agentkit/config/loader.py +108 -0
- agentkit/config/provider_defaults.py +96 -0
- agentkit/config/schema.py +148 -0
- agentkit/constants.py +21 -0
- agentkit/errors.py +58 -0
- agentkit/llm/__init__.py +53 -0
- agentkit/llm/base.py +36 -0
- agentkit/llm/factory.py +27 -0
- agentkit/llm/providers/__init__.py +15 -0
- agentkit/llm/providers/anthropic_provider.py +371 -0
- agentkit/llm/providers/gemini_provider.py +396 -0
- agentkit/llm/providers/openai_provider.py +881 -0
- agentkit/llm/providers/qwen_provider.py +34 -0
- agentkit/llm/providers/vllm_provider.py +47 -0
- agentkit/llm/types.py +215 -0
- agentkit/llm/usage.py +72 -0
- agentkit/py.typed +0 -0
- agentkit/runlog/__init__.py +15 -0
- agentkit/runlog/events.py +67 -0
- agentkit/runlog/jsonl.py +90 -0
- agentkit/runlog/recorder.py +94 -0
- agentkit/runlog/sinks.py +15 -0
- agentkit/tools/__init__.py +16 -0
- agentkit/tools/base.py +139 -0
- agentkit/tools/library/__init__.py +8 -0
- agentkit/tools/library/_fs_common.py +330 -0
- agentkit/tools/library/create_file.py +168 -0
- agentkit/tools/library/fs_tools.py +21 -0
- agentkit/tools/library/str_replace.py +241 -0
- agentkit/tools/library/view.py +372 -0
- agentkit/tools/library/word_count.py +138 -0
- agentkit/tools/loader.py +81 -0
- agentkit/tools/registry.py +284 -0
- agentkit/tools/types.py +98 -0
- agentkit/workspace/__init__.py +6 -0
- agentkit/workspace/fs.py +288 -0
- agentkit/workspace/layout.py +33 -0
- base_agentkit-0.1.0.dist-info/METADATA +142 -0
- base_agentkit-0.1.0.dist-info/RECORD +51 -0
- base_agentkit-0.1.0.dist-info/WHEEL +4 -0
- base_agentkit-0.1.0.dist-info/entry_points.txt +3 -0
- base_agentkit-0.1.0.dist-info/licenses/LICENSE +183 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tool runtime filtering and model-call bridging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from agentkit.llm.types import ToolCallItem, ToolResultItem, UnifiedToolSpec
|
|
9
|
+
from agentkit.tools.registry import ToolRegistry
|
|
10
|
+
from agentkit.tools.types import ToolCallOutcome, ToolInvocation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentToolRuntime:
|
|
14
|
+
"""Expose allowed tool schemas and execute tool calls for the agent loop."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, registry: ToolRegistry, allowed_tools: Sequence[str] | None = None
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Create a runtime view over the registry filtered by agent config."""
|
|
20
|
+
self._registry = registry
|
|
21
|
+
if allowed_tools is None:
|
|
22
|
+
self._allowed = set(registry.list_names())
|
|
23
|
+
else:
|
|
24
|
+
self._allowed = set(allowed_tools)
|
|
25
|
+
|
|
26
|
+
def schemas(self) -> list[UnifiedToolSpec]:
|
|
27
|
+
"""Return schemas for tools currently allowed to the agent."""
|
|
28
|
+
raw_schemas = self._registry.schemas(self._allowed)
|
|
29
|
+
return [
|
|
30
|
+
UnifiedToolSpec(
|
|
31
|
+
name=str(schema.get("name") or ""),
|
|
32
|
+
description=str(schema.get("description") or ""),
|
|
33
|
+
parameters=dict(schema.get("parameters") or {}),
|
|
34
|
+
)
|
|
35
|
+
for schema in raw_schemas
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def execute(self, call: ToolCallItem) -> ToolCallOutcome:
|
|
39
|
+
"""Execute one tool call and return the canonical outcome."""
|
|
40
|
+
if call.name not in self._allowed:
|
|
41
|
+
return ToolCallOutcome(
|
|
42
|
+
call_id=call.call_id,
|
|
43
|
+
name=call.name,
|
|
44
|
+
arguments=dict(call.arguments),
|
|
45
|
+
error=f"Tool not allowed by current agent config: {call.name}",
|
|
46
|
+
model_payload={
|
|
47
|
+
"error": {
|
|
48
|
+
"code": "tool_not_allowed",
|
|
49
|
+
"message": f"Tool '{call.name}' is not allowed by the current agent config.",
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
return self._registry.execute(
|
|
54
|
+
ToolInvocation(
|
|
55
|
+
name=call.name,
|
|
56
|
+
arguments=call.arguments,
|
|
57
|
+
call_id=call.call_id,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def build_result_item(self, outcome: ToolCallOutcome) -> ToolResultItem:
|
|
62
|
+
"""Build the model-facing transcript item for a completed tool call."""
|
|
63
|
+
payload = outcome.model_payload
|
|
64
|
+
if payload is None:
|
|
65
|
+
if outcome.is_error:
|
|
66
|
+
payload = {"error": {"message": outcome.error or "Tool execution failed."}}
|
|
67
|
+
else:
|
|
68
|
+
payload = {"output": outcome.output}
|
|
69
|
+
# Round-trip through JSON so provider adapters only ever see plain Python
|
|
70
|
+
# primitives, not custom mapping/list subclasses returned by a tool.
|
|
71
|
+
payload = json.loads(json.dumps(payload, ensure_ascii=False))
|
|
72
|
+
return ToolResultItem(
|
|
73
|
+
call_id=outcome.call_id or "",
|
|
74
|
+
tool_name=outcome.name,
|
|
75
|
+
payload=payload,
|
|
76
|
+
is_error=outcome.is_error,
|
|
77
|
+
)
|
agentkit/cli/__init__.py
ADDED
agentkit/cli/main.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""CLI entrypoint for running the agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from agentkit.agent.agent import Agent
|
|
11
|
+
from agentkit.config.loader import load_config
|
|
12
|
+
from agentkit.errors import AgentFrameworkError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
"""Create the command-line parser for the agent CLI.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
argparse.ArgumentParser: Parser configured with global flags and the ``run``
|
|
20
|
+
subcommand.
|
|
21
|
+
"""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="agentkit", description="LLM Agent Framework CLI"
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--config",
|
|
27
|
+
default="examples/config.openai.yaml",
|
|
28
|
+
help="Path to YAML/JSON config file.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
32
|
+
run_parser = subparsers.add_parser(
|
|
33
|
+
"run", help="Run one task with the configured agent."
|
|
34
|
+
)
|
|
35
|
+
run_parser.add_argument("--task", help="Task text to execute.")
|
|
36
|
+
run_parser.add_argument("--task-file", help="Read task text from file.")
|
|
37
|
+
run_parser.add_argument(
|
|
38
|
+
"--report-json", help="Optional path to write run report JSON."
|
|
39
|
+
)
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
"""Parse arguments and dispatch to the selected CLI command.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
None: The process exits via normal flow or ``SystemExit``.
|
|
48
|
+
"""
|
|
49
|
+
parser = build_parser()
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
|
|
52
|
+
if args.command == "run":
|
|
53
|
+
_run_command(args)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_command(args: argparse.Namespace) -> None:
|
|
57
|
+
"""Execute the ``run`` command using the parsed CLI arguments.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args: Parsed command namespace from :func:`build_parser`.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
None: Prints the final assistant output to stdout.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
SystemExit: Raised with code ``2`` when agent execution fails with a framework
|
|
67
|
+
error.
|
|
68
|
+
"""
|
|
69
|
+
task = _load_task(args.task, args.task_file)
|
|
70
|
+
config = load_config(args.config)
|
|
71
|
+
agent = Agent.from_config(config)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
report = agent.run(task)
|
|
75
|
+
except AgentFrameworkError as exc:
|
|
76
|
+
print(f"[agent-error] {exc}", file=sys.stderr)
|
|
77
|
+
raise SystemExit(2) from exc
|
|
78
|
+
|
|
79
|
+
if args.report_json:
|
|
80
|
+
Path(args.report_json).write_text(
|
|
81
|
+
json.dumps(report.to_dict(), ensure_ascii=False, indent=2),
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
print(report.final_output)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_task(task: str | None, task_file: str | None) -> str:
|
|
88
|
+
"""Resolve task text from CLI flags.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
task: Inline task text supplied via ``--task``.
|
|
92
|
+
task_file: Optional path supplied via ``--task-file``.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: The task string to send to the agent.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
SystemExit: If neither ``task`` nor ``task_file`` is provided.
|
|
99
|
+
"""
|
|
100
|
+
if task:
|
|
101
|
+
return task
|
|
102
|
+
if task_file:
|
|
103
|
+
return Path(task_file).read_text(encoding="utf-8")
|
|
104
|
+
raise SystemExit("Please provide --task or --task-file.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Public configuration loading and schema exports."""
|
|
2
|
+
|
|
3
|
+
from .loader import load_config
|
|
4
|
+
from .schema import (
|
|
5
|
+
AgentConfig,
|
|
6
|
+
AgentkitConfig,
|
|
7
|
+
BudgetConfig,
|
|
8
|
+
ProviderConfig,
|
|
9
|
+
RunLogConfig,
|
|
10
|
+
ToolConfig,
|
|
11
|
+
WorkspaceConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentConfig",
|
|
16
|
+
"AgentkitConfig",
|
|
17
|
+
"BudgetConfig",
|
|
18
|
+
"ProviderConfig",
|
|
19
|
+
"RunLogConfig",
|
|
20
|
+
"ToolConfig",
|
|
21
|
+
"WorkspaceConfig",
|
|
22
|
+
"load_config",
|
|
23
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Load and validate configuration from YAML/JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from agentkit.config.provider_defaults import apply_provider_config_defaults
|
|
14
|
+
from agentkit.config.schema import (
|
|
15
|
+
AgentConfig,
|
|
16
|
+
AgentkitConfig,
|
|
17
|
+
BudgetConfig,
|
|
18
|
+
ProviderConfig,
|
|
19
|
+
RunLogConfig,
|
|
20
|
+
ToolConfig,
|
|
21
|
+
WorkspaceConfig,
|
|
22
|
+
)
|
|
23
|
+
from agentkit.errors import ConfigError
|
|
24
|
+
|
|
25
|
+
_ENV_PATTERN = re.compile(r"^\$\{([A-Z0-9_]+)\}$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_config(
|
|
29
|
+
path: str | Path,
|
|
30
|
+
*,
|
|
31
|
+
overrides: Mapping[str, Any] | None = None,
|
|
32
|
+
) -> AgentkitConfig:
|
|
33
|
+
"""Load, merge, expand, and validate framework configuration."""
|
|
34
|
+
raw = _read_raw_config(path)
|
|
35
|
+
if overrides:
|
|
36
|
+
raw = _deep_merge(raw, dict(overrides))
|
|
37
|
+
raw = _expand_env(raw)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
workspace = WorkspaceConfig(**raw.get("workspace", {}))
|
|
41
|
+
provider = ProviderConfig(**raw.get("provider", {}))
|
|
42
|
+
budget = BudgetConfig(**raw.get("agent", {}).get("budget", {}))
|
|
43
|
+
|
|
44
|
+
agent_data = dict(raw.get("agent", {}))
|
|
45
|
+
agent_data.pop("budget", None)
|
|
46
|
+
agent = AgentConfig(budget=budget, **agent_data)
|
|
47
|
+
|
|
48
|
+
tools = ToolConfig(**raw.get("tools", {}))
|
|
49
|
+
runlog = RunLogConfig(**raw.get("runlog", {}))
|
|
50
|
+
except TypeError as exc:
|
|
51
|
+
raise ConfigError(f"Invalid configuration fields: {exc}") from exc
|
|
52
|
+
|
|
53
|
+
apply_provider_config_defaults(provider)
|
|
54
|
+
return AgentkitConfig(
|
|
55
|
+
workspace=workspace,
|
|
56
|
+
provider=provider,
|
|
57
|
+
agent=agent,
|
|
58
|
+
tools=tools,
|
|
59
|
+
runlog=runlog,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_raw_config(path: str | Path) -> dict[str, Any]:
|
|
64
|
+
"""Read and parse a raw YAML/JSON configuration mapping."""
|
|
65
|
+
config_path = Path(path)
|
|
66
|
+
if not config_path.exists():
|
|
67
|
+
raise ConfigError(f"Config not found: {config_path}")
|
|
68
|
+
text = config_path.read_text(encoding="utf-8")
|
|
69
|
+
suffix = config_path.suffix.lower()
|
|
70
|
+
|
|
71
|
+
if suffix in {".yaml", ".yml"}:
|
|
72
|
+
loaded = yaml.safe_load(text) or {}
|
|
73
|
+
elif suffix == ".json":
|
|
74
|
+
loaded = json.loads(text)
|
|
75
|
+
else:
|
|
76
|
+
raise ConfigError(f"Unsupported config format: {config_path.suffix}")
|
|
77
|
+
|
|
78
|
+
if not isinstance(loaded, dict):
|
|
79
|
+
raise ConfigError("Root config must be a mapping/object.")
|
|
80
|
+
return loaded
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _expand_env(value: Any) -> Any:
|
|
84
|
+
"""Recursively substitute ``${ENV_VAR}`` string placeholders."""
|
|
85
|
+
if isinstance(value, dict):
|
|
86
|
+
return {k: _expand_env(v) for k, v in value.items()}
|
|
87
|
+
if isinstance(value, list):
|
|
88
|
+
return [_expand_env(v) for v in value]
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
match = _ENV_PATTERN.match(value.strip())
|
|
91
|
+
if match:
|
|
92
|
+
return os.getenv(match.group(1))
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
"""Recursively merge two mapping trees with ``override`` precedence."""
|
|
98
|
+
merged = dict(base)
|
|
99
|
+
for key, value in override.items():
|
|
100
|
+
if (
|
|
101
|
+
key in merged
|
|
102
|
+
and isinstance(merged[key], dict)
|
|
103
|
+
and isinstance(value, Mapping)
|
|
104
|
+
):
|
|
105
|
+
merged[key] = _deep_merge(merged[key], dict(value))
|
|
106
|
+
else:
|
|
107
|
+
merged[key] = value
|
|
108
|
+
return merged
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Provider-specific default metadata and normalization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from agentkit.config.schema import ProviderConfig, ProviderKind
|
|
9
|
+
from agentkit.errors import ConfigError
|
|
10
|
+
|
|
11
|
+
DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
|
|
12
|
+
DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|
13
|
+
DEFAULT_QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
14
|
+
DEFAULT_VLLM_BASE_URL = "http://localhost:8000/v1"
|
|
15
|
+
|
|
16
|
+
DEFAULT_API_KEY_ENV_BY_PROVIDER: dict[ProviderKind, str] = {
|
|
17
|
+
"openai": "OPENAI_API_KEY",
|
|
18
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
19
|
+
"gemini": "GEMINI_API_KEY",
|
|
20
|
+
"vllm": "VLLM_API_KEY",
|
|
21
|
+
"qwen": "DASHSCOPE_API_KEY",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL_BY_PROVIDER: dict[ProviderKind, str] = {
|
|
25
|
+
"anthropic": DEFAULT_ANTHROPIC_BASE_URL,
|
|
26
|
+
"gemini": DEFAULT_GEMINI_BASE_URL,
|
|
27
|
+
"qwen": DEFAULT_QWEN_BASE_URL,
|
|
28
|
+
"vllm": DEFAULT_VLLM_BASE_URL,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ProviderDefaults:
|
|
34
|
+
"""Resolved default metadata for one provider kind.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
api_key_env: Default environment variable name for the API key.
|
|
38
|
+
base_url: Default base URL when the provider has a fixed endpoint.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
api_key_env: str
|
|
42
|
+
base_url: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def defaults_for_provider(kind: ProviderKind) -> ProviderDefaults:
|
|
46
|
+
"""Return the shared default metadata for one provider kind."""
|
|
47
|
+
if kind not in DEFAULT_API_KEY_ENV_BY_PROVIDER:
|
|
48
|
+
raise ConfigError(f"Unsupported provider kind: {kind}")
|
|
49
|
+
return ProviderDefaults(
|
|
50
|
+
api_key_env=DEFAULT_API_KEY_ENV_BY_PROVIDER[kind],
|
|
51
|
+
base_url=DEFAULT_BASE_URL_BY_PROVIDER.get(kind),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def apply_provider_config_defaults(
|
|
56
|
+
config: ProviderConfig,
|
|
57
|
+
) -> ProviderConfig:
|
|
58
|
+
"""Mutate a provider config in place with shared defaults and env resolution.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: Provider config to normalize.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
ProviderConfig: The same config object after normalization.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
agentkit.errors.ConfigError: If the provider still has no API key after
|
|
68
|
+
applying defaults.
|
|
69
|
+
"""
|
|
70
|
+
defaults = defaults_for_provider(config.kind)
|
|
71
|
+
|
|
72
|
+
if not config.api_key_env:
|
|
73
|
+
config.api_key_env = defaults.api_key_env
|
|
74
|
+
if not config.base_url and defaults.base_url:
|
|
75
|
+
config.base_url = defaults.base_url
|
|
76
|
+
|
|
77
|
+
if config.api_key is None and config.api_key_env:
|
|
78
|
+
config.api_key = os.getenv(config.api_key_env)
|
|
79
|
+
|
|
80
|
+
if not config.api_key:
|
|
81
|
+
if config.kind == "vllm" and is_localhost_base_url(config.base_url):
|
|
82
|
+
return config
|
|
83
|
+
env_name = config.api_key_env or defaults.api_key_env
|
|
84
|
+
raise ConfigError(
|
|
85
|
+
f"Missing API key. Set {env_name} or provider.api_key/provider.api_key_env in config."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return config
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_localhost_base_url(base_url: str | None) -> bool:
|
|
92
|
+
"""Return whether the configured base URL targets a local development server."""
|
|
93
|
+
if not base_url:
|
|
94
|
+
return False
|
|
95
|
+
lowered = base_url.lower()
|
|
96
|
+
return "localhost" in lowered or "127.0.0.1" in lowered
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Dataclass schemas for runtime configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from agentkit.constants import (
|
|
9
|
+
DEFAULT_MAX_INPUT_CHARS,
|
|
10
|
+
DEFAULT_MAX_STEPS,
|
|
11
|
+
DEFAULT_TIME_BUDGET_S,
|
|
12
|
+
)
|
|
13
|
+
from agentkit.errors import ConfigError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class WorkspaceConfig:
|
|
18
|
+
"""Workspace-level settings."""
|
|
19
|
+
|
|
20
|
+
root: str = "./workspace"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ProviderKind = Literal["openai", "anthropic", "gemini", "vllm", "qwen"]
|
|
24
|
+
OpenAIApiVariant = Literal["responses", "chat_completions"]
|
|
25
|
+
ConversationMode = Literal["auto", "client", "server"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class ProviderConfig:
|
|
30
|
+
"""LLM provider configuration."""
|
|
31
|
+
|
|
32
|
+
kind: ProviderKind = "openai"
|
|
33
|
+
model: str = "gpt-5"
|
|
34
|
+
openai_api_variant: OpenAIApiVariant = "responses"
|
|
35
|
+
conversation_mode: ConversationMode = "auto"
|
|
36
|
+
temperature: float | None = 0.2
|
|
37
|
+
timeout_s: int = 60
|
|
38
|
+
retries: int = 2
|
|
39
|
+
api_key: str | None = None
|
|
40
|
+
api_key_env: str | None = None
|
|
41
|
+
base_url: str | None = None
|
|
42
|
+
reasoning_effort: str | None = None
|
|
43
|
+
enable_thinking: bool = True
|
|
44
|
+
thinking_budget: int | None = None
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""Validate provider-specific invariants and cross-field combinations."""
|
|
48
|
+
if self.timeout_s <= 0:
|
|
49
|
+
raise ConfigError("provider.timeout_s must be > 0")
|
|
50
|
+
if self.retries < 0:
|
|
51
|
+
raise ConfigError("provider.retries must be >= 0")
|
|
52
|
+
if self.kind not in {"openai", "anthropic", "gemini", "vllm", "qwen"}:
|
|
53
|
+
raise ConfigError(f"Unsupported provider kind: {self.kind}")
|
|
54
|
+
if self.thinking_budget is not None and self.thinking_budget <= 0:
|
|
55
|
+
raise ConfigError("provider.thinking_budget must be > 0 when provided.")
|
|
56
|
+
|
|
57
|
+
if self.openai_api_variant not in {"responses", "chat_completions"}:
|
|
58
|
+
raise ConfigError(
|
|
59
|
+
"provider.openai_api_variant must be 'responses' or 'chat_completions'."
|
|
60
|
+
)
|
|
61
|
+
if self.conversation_mode not in {"auto", "client", "server"}:
|
|
62
|
+
raise ConfigError(
|
|
63
|
+
"provider.conversation_mode must be 'auto', 'client', or 'server'."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
self.kind in {"anthropic", "gemini"}
|
|
68
|
+
and self.openai_api_variant != "responses"
|
|
69
|
+
):
|
|
70
|
+
raise ConfigError(
|
|
71
|
+
"provider.openai_api_variant is only configurable for kind=openai; "
|
|
72
|
+
"kind=anthropic/gemini do not use it."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
self.kind in {"vllm", "qwen"}
|
|
77
|
+
and self.openai_api_variant != "chat_completions"
|
|
78
|
+
):
|
|
79
|
+
raise ConfigError(
|
|
80
|
+
f"provider.openai_api_variant for kind={self.kind} must be 'chat_completions'."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if self.conversation_mode == "server" and not (
|
|
84
|
+
self.kind == "openai" and self.openai_api_variant == "responses"
|
|
85
|
+
):
|
|
86
|
+
raise ConfigError(
|
|
87
|
+
"provider.conversation_mode='server' is only supported for "
|
|
88
|
+
"kind=openai with openai_api_variant='responses'."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class BudgetConfig:
|
|
94
|
+
"""Runtime budget limits for one task execution."""
|
|
95
|
+
|
|
96
|
+
max_steps: int = DEFAULT_MAX_STEPS
|
|
97
|
+
time_budget_s: int = DEFAULT_TIME_BUDGET_S
|
|
98
|
+
max_input_chars: int = DEFAULT_MAX_INPUT_CHARS
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
"""Validate that each configured runtime limit is strictly positive."""
|
|
102
|
+
if self.max_steps <= 0:
|
|
103
|
+
raise ConfigError("agent.budget.max_steps must be > 0")
|
|
104
|
+
if self.time_budget_s <= 0:
|
|
105
|
+
raise ConfigError("agent.budget.time_budget_s must be > 0")
|
|
106
|
+
if self.max_input_chars <= 0:
|
|
107
|
+
raise ConfigError("agent.budget.max_input_chars must be > 0")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(slots=True)
|
|
111
|
+
class AgentConfig:
|
|
112
|
+
"""Agent behavior configuration."""
|
|
113
|
+
|
|
114
|
+
system_prompt: str = "You are a helpful agent. Use tools when needed."
|
|
115
|
+
budget: BudgetConfig = field(default_factory=BudgetConfig)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(slots=True)
|
|
119
|
+
class ToolConfig:
|
|
120
|
+
"""Tool exposure configuration."""
|
|
121
|
+
|
|
122
|
+
allowed: list[str] = field(default_factory=list)
|
|
123
|
+
entries: list[str] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(slots=True)
|
|
127
|
+
class RunLogConfig:
|
|
128
|
+
"""Run log projection settings."""
|
|
129
|
+
|
|
130
|
+
enabled: bool = True
|
|
131
|
+
redact: bool = True
|
|
132
|
+
max_text_chars: int = 20_000
|
|
133
|
+
|
|
134
|
+
def __post_init__(self) -> None:
|
|
135
|
+
"""Validate redaction and truncation settings for run-log output."""
|
|
136
|
+
if self.max_text_chars <= 0:
|
|
137
|
+
raise ConfigError("runlog.max_text_chars must be > 0")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(slots=True)
|
|
141
|
+
class AgentkitConfig:
|
|
142
|
+
"""Top-level configuration container."""
|
|
143
|
+
|
|
144
|
+
workspace: WorkspaceConfig = field(default_factory=WorkspaceConfig)
|
|
145
|
+
provider: ProviderConfig = field(default_factory=ProviderConfig)
|
|
146
|
+
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
147
|
+
tools: ToolConfig = field(default_factory=ToolConfig)
|
|
148
|
+
runlog: RunLogConfig = field(default_factory=RunLogConfig)
|
agentkit/constants.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Project-level constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
DEFAULT_WORKSPACE_DIRS: tuple[str, ...] = ("logs",)
|
|
8
|
+
DEFAULT_RUNLOG_PATH = Path("logs/run.jsonl")
|
|
9
|
+
DEFAULT_ENCODING = "utf-8"
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_STEPS = 20
|
|
12
|
+
DEFAULT_TIME_BUDGET_S = 300
|
|
13
|
+
DEFAULT_MAX_INPUT_CHARS = 20_000
|
|
14
|
+
|
|
15
|
+
SENSITIVE_KEYS: tuple[str, ...] = (
|
|
16
|
+
"api_key",
|
|
17
|
+
"apikey",
|
|
18
|
+
"secret",
|
|
19
|
+
"password",
|
|
20
|
+
"authorization",
|
|
21
|
+
)
|
agentkit/errors.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Custom framework exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentFrameworkError(Exception):
|
|
10
|
+
"""Base class for all framework-specific exceptions."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigError(AgentFrameworkError):
|
|
14
|
+
"""Raised when configuration loading or validation fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkspaceError(AgentFrameworkError):
|
|
18
|
+
"""Raised for workspace path isolation and filesystem operation errors."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolError(AgentFrameworkError):
|
|
22
|
+
"""Raised for tool registration, validation, or execution failures."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ProviderIssueCategory = Literal[
|
|
26
|
+
"auth",
|
|
27
|
+
"rate_limit",
|
|
28
|
+
"invalid_request",
|
|
29
|
+
"timeout",
|
|
30
|
+
"upstream",
|
|
31
|
+
"safety",
|
|
32
|
+
"parse",
|
|
33
|
+
"unknown",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(slots=True)
|
|
38
|
+
class ProviderIssue:
|
|
39
|
+
"""Structured provider failure metadata for logging and retry decisions."""
|
|
40
|
+
|
|
41
|
+
category: ProviderIssueCategory
|
|
42
|
+
http_status: int | None = None
|
|
43
|
+
provider_code: str | None = None
|
|
44
|
+
retryable: bool = False
|
|
45
|
+
raw: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProviderError(AgentFrameworkError):
|
|
49
|
+
"""Raised for model provider request failures or invalid responses."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, message: str, *, issue: ProviderIssue | None = None) -> None:
|
|
52
|
+
"""Attach optional structured provider metadata to the raised error."""
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.issue = issue
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BudgetExceededError(AgentFrameworkError):
|
|
58
|
+
"""Raised when runtime step or elapsed-time budget is exceeded."""
|