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.
Files changed (51) hide show
  1. agentkit/__init__.py +35 -0
  2. agentkit/agent/__init__.py +7 -0
  3. agentkit/agent/agent.py +368 -0
  4. agentkit/agent/budgets.py +48 -0
  5. agentkit/agent/report.py +166 -0
  6. agentkit/agent/tool_runtime.py +77 -0
  7. agentkit/cli/__init__.py +5 -0
  8. agentkit/cli/main.py +108 -0
  9. agentkit/config/__init__.py +23 -0
  10. agentkit/config/loader.py +108 -0
  11. agentkit/config/provider_defaults.py +96 -0
  12. agentkit/config/schema.py +148 -0
  13. agentkit/constants.py +21 -0
  14. agentkit/errors.py +58 -0
  15. agentkit/llm/__init__.py +53 -0
  16. agentkit/llm/base.py +36 -0
  17. agentkit/llm/factory.py +27 -0
  18. agentkit/llm/providers/__init__.py +15 -0
  19. agentkit/llm/providers/anthropic_provider.py +371 -0
  20. agentkit/llm/providers/gemini_provider.py +396 -0
  21. agentkit/llm/providers/openai_provider.py +881 -0
  22. agentkit/llm/providers/qwen_provider.py +34 -0
  23. agentkit/llm/providers/vllm_provider.py +47 -0
  24. agentkit/llm/types.py +215 -0
  25. agentkit/llm/usage.py +72 -0
  26. agentkit/py.typed +0 -0
  27. agentkit/runlog/__init__.py +15 -0
  28. agentkit/runlog/events.py +67 -0
  29. agentkit/runlog/jsonl.py +90 -0
  30. agentkit/runlog/recorder.py +94 -0
  31. agentkit/runlog/sinks.py +15 -0
  32. agentkit/tools/__init__.py +16 -0
  33. agentkit/tools/base.py +139 -0
  34. agentkit/tools/library/__init__.py +8 -0
  35. agentkit/tools/library/_fs_common.py +330 -0
  36. agentkit/tools/library/create_file.py +168 -0
  37. agentkit/tools/library/fs_tools.py +21 -0
  38. agentkit/tools/library/str_replace.py +241 -0
  39. agentkit/tools/library/view.py +372 -0
  40. agentkit/tools/library/word_count.py +138 -0
  41. agentkit/tools/loader.py +81 -0
  42. agentkit/tools/registry.py +284 -0
  43. agentkit/tools/types.py +98 -0
  44. agentkit/workspace/__init__.py +6 -0
  45. agentkit/workspace/fs.py +288 -0
  46. agentkit/workspace/layout.py +33 -0
  47. base_agentkit-0.1.0.dist-info/METADATA +142 -0
  48. base_agentkit-0.1.0.dist-info/RECORD +51 -0
  49. base_agentkit-0.1.0.dist-info/WHEEL +4 -0
  50. base_agentkit-0.1.0.dist-info/entry_points.txt +3 -0
  51. base_agentkit-0.1.0.dist-info/licenses/LICENSE +183 -0
@@ -0,0 +1,53 @@
1
+ """Public LLM provider and type exports."""
2
+
3
+ from .base import BaseLLMProvider
4
+ from .factory import build_provider
5
+ from .providers.anthropic_provider import AnthropicProvider
6
+ from .providers.gemini_provider import GeminiProvider
7
+ from .providers.openai_provider import OpenAIProvider
8
+ from .providers.qwen_provider import QwenProvider
9
+ from .providers.vllm_provider import VLLMProvider
10
+ from .types import (
11
+ CompletionReason,
12
+ ConversationItem,
13
+ ConversationMode,
14
+ ConversationState,
15
+ GenerationOptions,
16
+ MessageItem,
17
+ ProviderKind,
18
+ ReasoningItem,
19
+ StatePatch,
20
+ ToolCallItem,
21
+ ToolResultItem,
22
+ TurnStatus,
23
+ UnifiedLLMRequest,
24
+ UnifiedLLMResponse,
25
+ UnifiedToolSpec,
26
+ Usage,
27
+ )
28
+
29
+ __all__ = [
30
+ "AnthropicProvider",
31
+ "BaseLLMProvider",
32
+ "build_provider",
33
+ "CompletionReason",
34
+ "ConversationItem",
35
+ "ConversationMode",
36
+ "ConversationState",
37
+ "GenerationOptions",
38
+ "GeminiProvider",
39
+ "MessageItem",
40
+ "OpenAIProvider",
41
+ "ProviderKind",
42
+ "QwenProvider",
43
+ "ReasoningItem",
44
+ "StatePatch",
45
+ "ToolCallItem",
46
+ "ToolResultItem",
47
+ "TurnStatus",
48
+ "UnifiedLLMRequest",
49
+ "UnifiedLLMResponse",
50
+ "UnifiedToolSpec",
51
+ "Usage",
52
+ "VLLMProvider",
53
+ ]
agentkit/llm/base.py ADDED
@@ -0,0 +1,36 @@
1
+ """Base abstractions for unified LLM providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from agentkit.llm.types import (
8
+ ConversationItem,
9
+ MessageItem,
10
+ UnifiedLLMRequest,
11
+ UnifiedLLMResponse,
12
+ )
13
+
14
+
15
+ class BaseLLMProvider(ABC):
16
+ """Abstract interface implemented by provider adapters."""
17
+
18
+ model: str
19
+
20
+ @abstractmethod
21
+ def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
22
+ """Execute one non-streaming model turn and return unified output."""
23
+
24
+ def render_output_text(
25
+ self,
26
+ output_items: list[ConversationItem],
27
+ raw_response: dict[str, object] | None,
28
+ ) -> str:
29
+ """Render provider-specific human-facing text from output items."""
30
+ del raw_response
31
+ texts = [
32
+ item.text
33
+ for item in output_items
34
+ if isinstance(item, MessageItem) and item.role == "assistant"
35
+ ]
36
+ return "\n".join(texts).strip()
@@ -0,0 +1,27 @@
1
+ """Provider factory for constructing configured LLM backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentkit.config.schema import ProviderConfig
6
+ from agentkit.errors import ConfigError
7
+ from agentkit.llm.base import BaseLLMProvider
8
+ from agentkit.llm.providers.anthropic_provider import AnthropicProvider
9
+ from agentkit.llm.providers.gemini_provider import GeminiProvider
10
+ from agentkit.llm.providers.openai_provider import OpenAIProvider
11
+ from agentkit.llm.providers.qwen_provider import QwenProvider
12
+ from agentkit.llm.providers.vllm_provider import VLLMProvider
13
+
14
+
15
+ def build_provider(config: ProviderConfig) -> BaseLLMProvider:
16
+ """Create the configured provider implementation."""
17
+ if config.kind == "openai":
18
+ return OpenAIProvider(config)
19
+ if config.kind == "anthropic":
20
+ return AnthropicProvider(config)
21
+ if config.kind == "gemini":
22
+ return GeminiProvider(config)
23
+ if config.kind == "vllm":
24
+ return VLLMProvider(config)
25
+ if config.kind == "qwen":
26
+ return QwenProvider(config)
27
+ raise ConfigError(f"Unsupported provider kind: {config.kind}")
@@ -0,0 +1,15 @@
1
+ """Public provider implementation exports."""
2
+
3
+ from .anthropic_provider import AnthropicProvider
4
+ from .gemini_provider import GeminiProvider
5
+ from .openai_provider import OpenAIProvider
6
+ from .qwen_provider import QwenProvider
7
+ from .vllm_provider import VLLMProvider
8
+
9
+ __all__ = [
10
+ "AnthropicProvider",
11
+ "GeminiProvider",
12
+ "OpenAIProvider",
13
+ "QwenProvider",
14
+ "VLLMProvider",
15
+ ]
@@ -0,0 +1,371 @@
1
+ """Anthropic Messages API provider adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import requests
8
+
9
+ from agentkit.config.provider_defaults import DEFAULT_ANTHROPIC_BASE_URL
10
+ from agentkit.config.schema import ProviderConfig
11
+ from agentkit.errors import ProviderError, ProviderIssue
12
+ from agentkit.llm.base import BaseLLMProvider
13
+ from agentkit.llm.types import (
14
+ CompletionReason,
15
+ ConversationItem,
16
+ MessageItem,
17
+ ReasoningItem,
18
+ StatePatch,
19
+ ToolCallItem,
20
+ ToolResultItem,
21
+ TurnStatus,
22
+ UnifiedLLMRequest,
23
+ UnifiedLLMResponse,
24
+ Usage,
25
+ )
26
+
27
+ class AnthropicProvider(BaseLLMProvider):
28
+ """Anthropic Messages API adapter."""
29
+
30
+ def __init__(self, config: ProviderConfig) -> None:
31
+ self.config = config
32
+ self.model = config.model
33
+ self._session = requests.Session()
34
+
35
+ def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
36
+ payload = self._build_payload(req)
37
+
38
+ try:
39
+ response = self._session.post(
40
+ self._messages_endpoint,
41
+ headers=self._headers,
42
+ json=payload,
43
+ timeout=self.config.timeout_s,
44
+ )
45
+ except requests.Timeout as exc: # pragma: no cover - network specific
46
+ raise ProviderError(
47
+ f"Anthropic request timed out: {exc}",
48
+ issue=ProviderIssue(category="timeout", retryable=True),
49
+ ) from exc
50
+ except requests.RequestException as exc: # pragma: no cover - network specific
51
+ raise ProviderError(
52
+ f"Anthropic request failed: {exc}",
53
+ issue=ProviderIssue(category="upstream", retryable=True),
54
+ ) from exc
55
+
56
+ if response.status_code >= 400:
57
+ self._raise_http_error(response)
58
+
59
+ try:
60
+ body = response.json()
61
+ except ValueError as exc:
62
+ raise ProviderError(
63
+ "Anthropic response is not valid JSON.",
64
+ issue=ProviderIssue(category="parse", retryable=False),
65
+ ) from exc
66
+
67
+ return self._parse_response(body)
68
+
69
+ @property
70
+ def _messages_endpoint(self) -> str:
71
+ base = self.config.base_url or DEFAULT_ANTHROPIC_BASE_URL
72
+ if base.endswith("/v1/messages"):
73
+ return base
74
+ return f"{base.rstrip('/')}/v1/messages"
75
+
76
+ @property
77
+ def _headers(self) -> dict[str, str]:
78
+ headers = {
79
+ "content-type": "application/json",
80
+ "anthropic-version": "2023-06-01",
81
+ }
82
+ if self.config.api_key:
83
+ headers["x-api-key"] = self.config.api_key
84
+ return headers
85
+
86
+ def render_output_text(
87
+ self,
88
+ output_items: list[ConversationItem],
89
+ raw_response: dict[str, object] | None,
90
+ ) -> str:
91
+ del raw_response
92
+ texts = [
93
+ item.text
94
+ for item in output_items
95
+ if isinstance(item, MessageItem) and item.role == "assistant" and item.text
96
+ ]
97
+ return "\n".join(texts).strip()
98
+
99
+ def _build_payload(self, req: UnifiedLLMRequest) -> dict[str, Any]:
100
+ messages = self._compile_messages(req.state.history + req.inputs)
101
+
102
+ payload: dict[str, Any] = {
103
+ "model": req.model,
104
+ "messages": messages,
105
+ "max_tokens": req.options.max_output_tokens or 1024,
106
+ }
107
+
108
+ if req.tools:
109
+ payload["tools"] = [
110
+ {
111
+ "name": tool.name,
112
+ "description": tool.description,
113
+ "input_schema": tool.parameters,
114
+ }
115
+ for tool in req.tools
116
+ ]
117
+
118
+ system_prompt = self._compile_system(req)
119
+ if system_prompt:
120
+ payload["system"] = system_prompt
121
+
122
+ temperature = (
123
+ req.options.temperature
124
+ if req.options.temperature is not None
125
+ else self.config.temperature
126
+ )
127
+ if temperature is not None:
128
+ payload["temperature"] = temperature
129
+
130
+ if req.options.stop_sequences:
131
+ payload["stop_sequences"] = list(req.options.stop_sequences)
132
+
133
+ return payload
134
+
135
+ def _compile_system(self, req: UnifiedLLMRequest) -> str:
136
+ return req.instructions.strip()
137
+
138
+ def _compile_messages(self, items: list[ConversationItem]) -> list[dict[str, Any]]:
139
+ raw_messages: list[dict[str, Any]] = []
140
+ for item in items:
141
+ message = self._item_to_message(item)
142
+ if message is not None:
143
+ raw_messages.append(message)
144
+
145
+ return self._merge_consecutive_roles(raw_messages)
146
+
147
+ def _item_to_message(self, item: ConversationItem) -> dict[str, Any] | None:
148
+ if isinstance(item, MessageItem):
149
+ return {
150
+ "role": item.role,
151
+ "content": [{"type": "text", "text": item.text}],
152
+ }
153
+
154
+ if isinstance(item, ToolCallItem):
155
+ return {
156
+ "role": "assistant",
157
+ "content": [
158
+ {
159
+ "type": "tool_use",
160
+ "id": item.call_id,
161
+ "name": item.name,
162
+ "input": item.arguments,
163
+ }
164
+ ],
165
+ }
166
+
167
+ if isinstance(item, ToolResultItem):
168
+ return {
169
+ "role": "user",
170
+ "content": [
171
+ {
172
+ "type": "tool_result",
173
+ "tool_use_id": item.call_id,
174
+ "content": item.output_text,
175
+ "is_error": item.is_error,
176
+ }
177
+ ],
178
+ }
179
+
180
+ if item.replay_hint and item.raw_item:
181
+ block_type = str(item.raw_item.get("type") or "")
182
+ if block_type in {"thinking", "redacted_thinking"}:
183
+ return {"role": "assistant", "content": [item.raw_item]}
184
+
185
+ return None
186
+
187
+ def _merge_consecutive_roles(
188
+ self, messages: list[dict[str, Any]]
189
+ ) -> list[dict[str, Any]]:
190
+ if not messages:
191
+ return []
192
+
193
+ merged: list[dict[str, Any]] = []
194
+ for message in messages:
195
+ role = str(message.get("role") or "user")
196
+ content = list(message.get("content") or [])
197
+ if merged and merged[-1].get("role") == role:
198
+ merged[-1].setdefault("content", [])
199
+ merged[-1]["content"].extend(content)
200
+ else:
201
+ merged.append({"role": role, "content": content})
202
+ return merged
203
+
204
+ def _parse_response(self, body: dict[str, Any]) -> UnifiedLLMResponse:
205
+ output_items: list[ConversationItem] = []
206
+
207
+ for block_raw in body.get("content") or []:
208
+ block = self._to_dict(block_raw)
209
+ block_type = str(block.get("type") or "")
210
+
211
+ if block_type == "text":
212
+ text = str(block.get("text") or "")
213
+ if text:
214
+ output_items.append(MessageItem(role="assistant", text=text))
215
+ continue
216
+
217
+ if block_type == "tool_use":
218
+ output_items.append(
219
+ ToolCallItem(
220
+ call_id=str(block.get("id") or ""),
221
+ name=str(block.get("name") or ""),
222
+ arguments=self._ensure_dict(block.get("input")),
223
+ )
224
+ )
225
+ continue
226
+
227
+ if block_type in {"thinking", "redacted_thinking"}:
228
+ thinking_text = block.get("thinking")
229
+ if not isinstance(thinking_text, str):
230
+ thinking_text = block.get("text") if isinstance(block.get("text"), str) else None
231
+ output_items.append(
232
+ ReasoningItem(
233
+ text=thinking_text,
234
+ summary=None,
235
+ raw_item=block,
236
+ replay_hint=True,
237
+ )
238
+ )
239
+
240
+ status, reason = self._map_status(body, output_items)
241
+
242
+ return UnifiedLLMResponse(
243
+ response_id=str(body.get("id") or "") or None,
244
+ status=status,
245
+ reason=reason,
246
+ output_items=output_items,
247
+ output_text=self.render_output_text(output_items, body),
248
+ usage=self._parse_usage(body),
249
+ state_patch=StatePatch(),
250
+ provider_name="anthropic",
251
+ raw_response=body,
252
+ )
253
+
254
+ def _map_status(
255
+ self,
256
+ body: dict[str, Any],
257
+ output_items: list[ConversationItem],
258
+ ) -> tuple[TurnStatus, CompletionReason]:
259
+ if any(isinstance(item, ToolCallItem) for item in output_items):
260
+ return "requires_tool", "tool_call"
261
+
262
+ stop_reason = str(body.get("stop_reason") or "")
263
+
264
+ mapping: dict[str, tuple[TurnStatus, CompletionReason]] = {
265
+ "end_turn": ("completed", "stop"),
266
+ "stop_sequence": ("completed", "stop"),
267
+ "max_tokens": ("incomplete", "max_tokens"),
268
+ "tool_use": ("requires_tool", "tool_call"),
269
+ "pause_turn": ("incomplete", "pause"),
270
+ "refusal": ("blocked", "refusal"),
271
+ "model_context_window_exceeded": ("incomplete", "context_window"),
272
+ }
273
+ if stop_reason in mapping:
274
+ return mapping[stop_reason]
275
+
276
+ if body.get("type") == "error":
277
+ return "failed", "error"
278
+
279
+ return "incomplete", "unknown"
280
+
281
+ def _parse_usage(self, body: dict[str, Any]) -> Usage:
282
+ usage = self._to_dict(body.get("usage") or {})
283
+ input_tokens = self._as_int(usage.get("input_tokens"))
284
+ output_tokens = self._as_int(usage.get("output_tokens"))
285
+
286
+ total_tokens: int | None = None
287
+ if input_tokens is not None and output_tokens is not None:
288
+ total_tokens = input_tokens + output_tokens
289
+
290
+ return Usage(
291
+ input_tokens=input_tokens,
292
+ output_tokens=output_tokens,
293
+ total_tokens=total_tokens,
294
+ cache_write_tokens=self._as_int(usage.get("cache_creation_input_tokens")),
295
+ cache_read_tokens=self._as_int(usage.get("cache_read_input_tokens")),
296
+ raw=usage or None,
297
+ )
298
+
299
+ def _raise_http_error(self, response: requests.Response) -> None:
300
+ body: dict[str, Any] | None = None
301
+ try:
302
+ parsed = response.json()
303
+ if isinstance(parsed, dict):
304
+ body = parsed
305
+ except ValueError:
306
+ body = None
307
+
308
+ status = response.status_code
309
+ category = "unknown"
310
+ retryable = False
311
+ provider_code: str | None = None
312
+
313
+ if status in {401, 403}:
314
+ category = "auth"
315
+ elif status == 429:
316
+ category = "rate_limit"
317
+ retryable = True
318
+ elif 400 <= status < 500:
319
+ category = "invalid_request"
320
+ elif status >= 500:
321
+ category = "upstream"
322
+ retryable = True
323
+
324
+ if body:
325
+ err = self._to_dict(body.get("error") or {})
326
+ provider_code = str(err.get("type") or err.get("code") or "") or None
327
+ err_message = str(err.get("message") or "").lower()
328
+ if "safety" in err_message or "policy" in err_message:
329
+ category = "safety"
330
+
331
+ raise ProviderError(
332
+ f"Anthropic request failed with status {status}.",
333
+ issue=ProviderIssue(
334
+ category=category,
335
+ http_status=status,
336
+ provider_code=provider_code,
337
+ retryable=retryable,
338
+ raw=body,
339
+ ),
340
+ )
341
+
342
+ def _to_dict(self, value: Any) -> dict[str, Any]:
343
+ if isinstance(value, dict):
344
+ return value
345
+ if hasattr(value, "model_dump"):
346
+ try:
347
+ dumped = value.model_dump(mode="python")
348
+ if isinstance(dumped, dict):
349
+ return dumped
350
+ except Exception:
351
+ pass
352
+ return {}
353
+
354
+ def _ensure_dict(self, value: Any) -> dict[str, Any]:
355
+ if isinstance(value, dict):
356
+ return value
357
+ return {}
358
+
359
+ def _as_int(self, value: Any) -> int | None:
360
+ if isinstance(value, bool):
361
+ return int(value)
362
+ if isinstance(value, int):
363
+ return value
364
+ if isinstance(value, float):
365
+ return int(value)
366
+ if isinstance(value, str):
367
+ try:
368
+ return int(value)
369
+ except ValueError:
370
+ return None
371
+ return None