RouteKitAI 0.1.0__py3-none-any.whl → 0.2.1__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.
- routekitai/__init__.py +1 -1
- routekitai/core/message.py +2 -2
- routekitai/core/policies.py +20 -14
- routekitai/core/policy.py +3 -0
- routekitai/core/policy_adapter.py +17 -2
- routekitai/core/runtime.py +57 -11
- routekitai/core/tool.py +2 -2
- routekitai/graphs/graph.py +2 -2
- routekitai/message.py +2 -2
- routekitai/providers/__init__.py +2 -0
- routekitai/providers/anthropic.py +25 -9
- routekitai/sandbox/permissions.py +2 -2
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/METADATA +58 -28
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/RECORD +18 -18
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/WHEEL +0 -0
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/entry_points.txt +0 -0
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {routekitai-0.1.0.dist-info → routekitai-0.2.1.dist-info}/top_level.txt +0 -0
routekitai/__init__.py
CHANGED
routekitai/core/message.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Message primitive for RouteKit."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class MessageRole(
|
|
9
|
+
class MessageRole(StrEnum):
|
|
10
10
|
"""Message role types."""
|
|
11
11
|
|
|
12
12
|
SYSTEM = "system"
|
routekitai/core/policies.py
CHANGED
|
@@ -25,7 +25,8 @@ class ReActPolicy(Policy):
|
|
|
25
25
|
Simple loop: model -> decide tool -> tool -> model -> final
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
max_iterations: int = 10
|
|
28
|
+
def __init__(self, max_iterations: int = 10) -> None:
|
|
29
|
+
self.max_iterations = max_iterations
|
|
29
30
|
|
|
30
31
|
async def plan(self, state: dict[str, Any]) -> list[Action]:
|
|
31
32
|
"""Plan next action in ReAct loop.
|
|
@@ -57,6 +58,7 @@ class ReActPolicy(Policy):
|
|
|
57
58
|
ToolAction(
|
|
58
59
|
tool_name=tc["name"],
|
|
59
60
|
tool_input=tc.get("arguments", {}),
|
|
61
|
+
tool_call_id=tc.get("id", ""),
|
|
60
62
|
)
|
|
61
63
|
for tc in last_message.tool_calls
|
|
62
64
|
]
|
|
@@ -120,6 +122,7 @@ class FunctionCallingPolicy(Policy):
|
|
|
120
122
|
ToolAction(
|
|
121
123
|
tool_name=tool_name,
|
|
122
124
|
tool_input=tc.get("arguments", {}),
|
|
125
|
+
tool_call_id=tc.get("id", ""),
|
|
123
126
|
)
|
|
124
127
|
)
|
|
125
128
|
|
|
@@ -178,6 +181,10 @@ class GraphPolicy(Policy, BaseModel):
|
|
|
178
181
|
# Execute graph
|
|
179
182
|
graph_result = await executor.execute(input_data=input_data)
|
|
180
183
|
|
|
184
|
+
# Store on runtime so callers can inspect steps and state (e.g. for logging)
|
|
185
|
+
if runtime is not None:
|
|
186
|
+
runtime._last_graph_result = graph_result
|
|
187
|
+
|
|
181
188
|
# Return final output as result
|
|
182
189
|
final_output = graph_result.get("state", {}).get("output") or graph_result.get(
|
|
183
190
|
"state", {}
|
|
@@ -302,19 +309,18 @@ class SupervisorPolicy(Policy, BaseModel):
|
|
|
302
309
|
if iteration >= self.max_iterations:
|
|
303
310
|
return [Final(output=Message.assistant("Max iterations reached"))]
|
|
304
311
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
subagent_result
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return [ModelAction(messages=[*messages, result_message])]
|
|
312
|
+
# Sub-agent just completed: finalize with its result (runtime set waiting_for_subagent=False)
|
|
313
|
+
subagent_result = state.get("subagent_result")
|
|
314
|
+
if subagent_result and not state.get("waiting_for_subagent"):
|
|
315
|
+
if isinstance(subagent_result, dict) and "output" in subagent_result:
|
|
316
|
+
content = subagent_result["output"]
|
|
317
|
+
if hasattr(content, "content"):
|
|
318
|
+
content = content.content
|
|
319
|
+
else:
|
|
320
|
+
content = str(content)
|
|
321
|
+
else:
|
|
322
|
+
content = str(subagent_result)
|
|
323
|
+
return [Final(output=Message.assistant(content))]
|
|
318
324
|
|
|
319
325
|
# If no messages, supervisor decides which agent to use
|
|
320
326
|
if not messages:
|
routekitai/core/policy.py
CHANGED
|
@@ -28,6 +28,9 @@ class ToolAction(Action):
|
|
|
28
28
|
action_type: str = Field(default="tool", description="Action type")
|
|
29
29
|
tool_name: str = Field(..., description="Tool name to execute")
|
|
30
30
|
tool_input: dict[str, Any] = Field(..., description="Tool input arguments")
|
|
31
|
+
tool_call_id: str = Field(
|
|
32
|
+
default="", description="Tool call ID from assistant message (for API compatibility)"
|
|
33
|
+
)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class Parallel(Action):
|
|
@@ -107,6 +107,7 @@ class PolicyAdapter(RuntimePolicy):
|
|
|
107
107
|
input_data={
|
|
108
108
|
"tool_name": action.tool_name,
|
|
109
109
|
"tool_args": action.tool_input,
|
|
110
|
+
"tool_call_id": action.tool_call_id,
|
|
110
111
|
},
|
|
111
112
|
)
|
|
112
113
|
)
|
|
@@ -122,12 +123,26 @@ class PolicyAdapter(RuntimePolicy):
|
|
|
122
123
|
input_data={
|
|
123
124
|
"tool_name": sub_action.tool_name,
|
|
124
125
|
"tool_args": sub_action.tool_input,
|
|
126
|
+
"tool_call_id": sub_action.tool_call_id,
|
|
125
127
|
},
|
|
126
128
|
)
|
|
127
129
|
)
|
|
128
130
|
# Other action types in parallel not yet supported
|
|
129
131
|
elif isinstance(action, Final):
|
|
130
|
-
# Final action - return
|
|
131
|
-
|
|
132
|
+
# Final action - return a "final" step so runtime uses this output
|
|
133
|
+
steps.append(
|
|
134
|
+
Step(
|
|
135
|
+
step_id=str(uuid.uuid4()),
|
|
136
|
+
step_type="final",
|
|
137
|
+
input_data={"output": action.output},
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return steps
|
|
132
141
|
|
|
133
142
|
return steps
|
|
143
|
+
|
|
144
|
+
async def reflect(self, state: dict[str, Any], observation: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
+
"""Delegate to the underlying policy's reflect if it has one."""
|
|
146
|
+
if hasattr(self.policy, "reflect") and callable(self.policy.reflect):
|
|
147
|
+
return await self.policy.reflect(state, observation)
|
|
148
|
+
return state.copy()
|
routekitai/core/runtime.py
CHANGED
|
@@ -107,6 +107,8 @@ class Runtime(BaseModel):
|
|
|
107
107
|
self._current_step: int = 0
|
|
108
108
|
self._total_steps: int = 0
|
|
109
109
|
self._progress_callbacks: list[Callable[[dict[str, Any]], None]] = []
|
|
110
|
+
# Last graph execution result (set by GraphPolicy for callers to inspect steps/state)
|
|
111
|
+
self._last_graph_result: dict[str, Any] | None = None
|
|
110
112
|
|
|
111
113
|
def register_agent(self, agent: "Agent") -> None:
|
|
112
114
|
"""Register an agent with the runtime.
|
|
@@ -244,16 +246,10 @@ class Runtime(BaseModel):
|
|
|
244
246
|
}
|
|
245
247
|
trace.add_event("run_completed", {"trace_id": trace_id, "result": result_dict})
|
|
246
248
|
|
|
247
|
-
# Export trace
|
|
249
|
+
# Export trace so it is on disk before returning (needed for replay and tests)
|
|
248
250
|
if exporter and self.trace_dir:
|
|
249
|
-
# Ensure directory exists before async export
|
|
250
251
|
self.trace_dir.mkdir(parents=True, exist_ok=True)
|
|
251
|
-
|
|
252
|
-
export_task = asyncio.create_task(exporter.export(trace))
|
|
253
|
-
# Store task reference for potential cleanup
|
|
254
|
-
# Note: Task will complete in background, errors are logged by exporter
|
|
255
|
-
# For testing, we could await here, but in production we want fire-and-forget
|
|
256
|
-
# The exporter now creates the directory itself, so this should work
|
|
252
|
+
await exporter.export(trace)
|
|
257
253
|
|
|
258
254
|
return result
|
|
259
255
|
except asyncio.CancelledError:
|
|
@@ -394,7 +390,13 @@ class Runtime(BaseModel):
|
|
|
394
390
|
step_results = await self._execute_steps_parallel(steps, agent, trace)
|
|
395
391
|
|
|
396
392
|
# Process step results
|
|
393
|
+
final_output_message: Message | None = None
|
|
394
|
+
last_model_response: ModelResponse | None = None
|
|
397
395
|
for step_result in step_results:
|
|
396
|
+
if step_result.step_type == "final":
|
|
397
|
+
if step_result.output_data and "output" in step_result.output_data:
|
|
398
|
+
final_output_message = step_result.output_data["output"]
|
|
399
|
+
break
|
|
398
400
|
if step_result.step_type == "model_call":
|
|
399
401
|
# Handle model response
|
|
400
402
|
if (
|
|
@@ -413,6 +415,11 @@ class Runtime(BaseModel):
|
|
|
413
415
|
if isinstance(response_data, dict)
|
|
414
416
|
else []
|
|
415
417
|
)
|
|
418
|
+
last_model_response = ModelResponse(
|
|
419
|
+
content=content,
|
|
420
|
+
tool_calls=None,
|
|
421
|
+
usage=None,
|
|
422
|
+
)
|
|
416
423
|
|
|
417
424
|
# Create assistant message with tool calls
|
|
418
425
|
tool_calls: list[dict[str, Any]] | None = None
|
|
@@ -445,13 +452,22 @@ class Runtime(BaseModel):
|
|
|
445
452
|
if step_result.input_data
|
|
446
453
|
else ""
|
|
447
454
|
)
|
|
455
|
+
tool_call_id = (
|
|
456
|
+
step_result.input_data.get("tool_call_id", "")
|
|
457
|
+
if step_result.input_data
|
|
458
|
+
else ""
|
|
459
|
+
)
|
|
448
460
|
tool_result = step_result.output_data["result"]
|
|
449
461
|
|
|
450
|
-
# Add tool result message
|
|
462
|
+
# Add tool result message (tool_call_id required by OpenAI API)
|
|
451
463
|
messages.append(
|
|
452
464
|
Message.tool(
|
|
453
465
|
f"Tool {tool_name} executed",
|
|
454
|
-
{
|
|
466
|
+
{
|
|
467
|
+
"result": tool_result,
|
|
468
|
+
"tool": tool_name,
|
|
469
|
+
"tool_call_id": tool_call_id,
|
|
470
|
+
},
|
|
455
471
|
)
|
|
456
472
|
)
|
|
457
473
|
|
|
@@ -495,6 +511,24 @@ class Runtime(BaseModel):
|
|
|
495
511
|
}
|
|
496
512
|
state["waiting_for_subagent"] = False
|
|
497
513
|
|
|
514
|
+
# Let policy reflect on step results (e.g. PlanExecutePolicy updates phase/plan)
|
|
515
|
+
if (
|
|
516
|
+
last_model_response is not None
|
|
517
|
+
and hasattr(policy, "reflect")
|
|
518
|
+
and callable(policy.reflect)
|
|
519
|
+
):
|
|
520
|
+
state.setdefault(
|
|
521
|
+
"phase", "planning"
|
|
522
|
+
) # so policies can detect first (planning) phase
|
|
523
|
+
observation = {"result": last_model_response}
|
|
524
|
+
reflected = await policy.reflect(state, observation)
|
|
525
|
+
if isinstance(reflected, dict):
|
|
526
|
+
state.update(reflected)
|
|
527
|
+
|
|
528
|
+
if final_output_message is not None:
|
|
529
|
+
output_message = final_output_message
|
|
530
|
+
break
|
|
531
|
+
|
|
498
532
|
iteration += 1
|
|
499
533
|
|
|
500
534
|
# Finalize if we hit max iterations
|
|
@@ -509,6 +543,14 @@ class Runtime(BaseModel):
|
|
|
509
543
|
output_message = Message.assistant(final_response.content)
|
|
510
544
|
messages.append(output_message)
|
|
511
545
|
|
|
546
|
+
# If final output has no content (e.g. model returned empty after tool use),
|
|
547
|
+
# use the last assistant message that had content so the run result is useful.
|
|
548
|
+
if not (output_message.content or "").strip() and messages:
|
|
549
|
+
for msg in reversed(messages):
|
|
550
|
+
if msg.role == MessageRole.ASSISTANT and (msg.content or "").strip():
|
|
551
|
+
output_message = msg
|
|
552
|
+
break
|
|
553
|
+
|
|
512
554
|
# Import here to avoid circular import
|
|
513
555
|
from routekitai.core.agent import RunResult
|
|
514
556
|
|
|
@@ -600,7 +642,10 @@ class Runtime(BaseModel):
|
|
|
600
642
|
)
|
|
601
643
|
|
|
602
644
|
try:
|
|
603
|
-
if step.step_type == "
|
|
645
|
+
if step.step_type == "final":
|
|
646
|
+
# Policy produced a final output (e.g. GraphPolicy); pass it through
|
|
647
|
+
step.output_data = {"output": step.input_data.get("output")}
|
|
648
|
+
elif step.step_type == "model_call":
|
|
604
649
|
# Check if in replay mode
|
|
605
650
|
if self._replay_mode and self._replay_trace:
|
|
606
651
|
# Match by sequential order using step_completed events
|
|
@@ -1394,6 +1439,7 @@ class DefaultPolicy(Policy):
|
|
|
1394
1439
|
input_data={
|
|
1395
1440
|
"tool_name": tool_call["name"],
|
|
1396
1441
|
"tool_args": tool_call.get("arguments", {}),
|
|
1442
|
+
"tool_call_id": tool_call.get("id", ""),
|
|
1397
1443
|
},
|
|
1398
1444
|
)
|
|
1399
1445
|
)
|
routekitai/core/tool.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Tool primitive for RouteKit."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from enum import
|
|
4
|
+
from enum import StrEnum
|
|
5
5
|
from typing import Any, TypeVar
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, create_model
|
|
@@ -12,7 +12,7 @@ TInput = TypeVar("TInput", bound=BaseModel)
|
|
|
12
12
|
TOutput = TypeVar("TOutput", bound=BaseModel)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class ToolPermission(
|
|
15
|
+
class ToolPermission(StrEnum):
|
|
16
16
|
"""Tool permission types."""
|
|
17
17
|
|
|
18
18
|
NETWORK = "network"
|
routekitai/graphs/graph.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Graph definition for agent orchestration."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from enum import
|
|
4
|
+
from enum import StrEnum
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class NodeType(
|
|
10
|
+
class NodeType(StrEnum):
|
|
11
11
|
"""Type of graph node."""
|
|
12
12
|
|
|
13
13
|
MODEL = "model" # Execute a model call
|
routekitai/message.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Message primitive for RouteKit."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class MessageRole(
|
|
9
|
+
class MessageRole(StrEnum):
|
|
10
10
|
"""Message role types."""
|
|
11
11
|
|
|
12
12
|
USER = "user"
|
routekitai/providers/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Model providers for RouteKit."""
|
|
2
2
|
|
|
3
|
+
from routekitai.providers.anthropic import AnthropicModel
|
|
3
4
|
from routekitai.providers.local import FakeModel
|
|
4
5
|
from routekitai.providers.openai import OpenAIChatModel
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
8
|
+
"AnthropicModel",
|
|
7
9
|
"OpenAIChatModel",
|
|
8
10
|
"FakeModel",
|
|
9
11
|
]
|
|
@@ -75,7 +75,11 @@ class AnthropicModel(Model):
|
|
|
75
75
|
return self._client
|
|
76
76
|
|
|
77
77
|
def _message_to_anthropic(self, message: Message) -> dict[str, Any]:
|
|
78
|
-
"""Convert routkitai Message to Anthropic format.
|
|
78
|
+
"""Convert routkitai Message to Anthropic format.
|
|
79
|
+
|
|
80
|
+
Anthropic requires all messages to have non-empty content except the
|
|
81
|
+
optional final assistant message. We ensure content is never empty.
|
|
82
|
+
"""
|
|
79
83
|
role_map = {
|
|
80
84
|
MessageRole.SYSTEM: "system",
|
|
81
85
|
MessageRole.USER: "user",
|
|
@@ -84,13 +88,15 @@ class AnthropicModel(Model):
|
|
|
84
88
|
# Anthropic doesn't support tool role messages directly
|
|
85
89
|
if message.role == MessageRole.TOOL:
|
|
86
90
|
# Convert tool messages to user messages with tool result
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
+
content = (
|
|
92
|
+
f"Tool result: {message.content}" if message.content else "Tool result: (no output)"
|
|
93
|
+
)
|
|
94
|
+
return {"role": "user", "content": content}
|
|
95
|
+
# API rejects empty or whitespace-only content; use a non-whitespace placeholder
|
|
96
|
+
content = message.content if (message.content and message.content.strip()) else "(no text)"
|
|
91
97
|
return {
|
|
92
98
|
"role": role_map.get(message.role, "user"),
|
|
93
|
-
"content":
|
|
99
|
+
"content": content,
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
def _tools_to_anthropic(self, tools: list[Tool]) -> list[dict[str, Any]]:
|
|
@@ -208,9 +214,19 @@ class AnthropicModel(Model):
|
|
|
208
214
|
)
|
|
209
215
|
|
|
210
216
|
except httpx.HTTPStatusError as e:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
msg = f"Anthropic API error: {e.response.status_code}"
|
|
218
|
+
try:
|
|
219
|
+
body = e.response.json()
|
|
220
|
+
err = body.get("error") or {}
|
|
221
|
+
if isinstance(err, dict) and err.get("message"):
|
|
222
|
+
msg = f"{msg} - {err['message']}"
|
|
223
|
+
else:
|
|
224
|
+
msg = f"{msg} - {e.response.text}"
|
|
225
|
+
except Exception:
|
|
226
|
+
msg = f"{msg} - {e.response.text}"
|
|
227
|
+
if e.response.status_code == 404 and "model" in msg.lower():
|
|
228
|
+
msg += " Use a model ID from https://docs.anthropic.com/en/api/models or set ANTHROPIC_MODEL."
|
|
229
|
+
raise ModelError(msg) from e
|
|
214
230
|
except Exception as e:
|
|
215
231
|
raise ModelError(f"Failed to call Anthropic API: {e}") from e
|
|
216
232
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Permission management for tool execution."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
from routekitai.core.tool import ToolPermission
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class PermissionLevel(
|
|
10
|
+
class PermissionLevel(StrEnum):
|
|
11
11
|
"""Permission levels for sandbox execution."""
|
|
12
12
|
|
|
13
13
|
NONE = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: RouteKitAI
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An agent development + orchestration framework
|
|
5
5
|
Author: Mohamed Ghassen Brahim
|
|
6
6
|
License: MIT
|
|
@@ -41,6 +41,9 @@ Requires-Dist: sentence-transformers>=2.2.0; extra == "optional"
|
|
|
41
41
|
Requires-Dist: faiss-cpu>=1.7.4; extra == "optional"
|
|
42
42
|
Requires-Dist: openai>=1.0.0; extra == "optional"
|
|
43
43
|
Requires-Dist: nest-asyncio>=1.5.0; extra == "optional"
|
|
44
|
+
Provides-Extra: docs
|
|
45
|
+
Requires-Dist: mkdocs>=1.5.0; extra == "docs"
|
|
46
|
+
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
44
47
|
Dynamic: license-file
|
|
45
48
|
|
|
46
49
|
# RouteKitAI
|
|
@@ -60,6 +63,8 @@ Dynamic: license-file
|
|
|
60
63
|
|
|
61
64
|
---
|
|
62
65
|
|
|
66
|
+
> **⚠️ Early stage**: RouteKitAI is still in very early development. APIs may change, and some features are experimental. Use with caution in production.
|
|
67
|
+
|
|
63
68
|
RouteKitAI is a Python framework for building AI agents with **graph-based orchestration**, **built-in tracing**, and **deterministic replay**. Unlike other frameworks, RouteKitAI treats observability and testability as first-class features from day one.
|
|
64
69
|
|
|
65
70
|
## ✨ Features
|
|
@@ -195,50 +200,54 @@ asyncio.run(main())
|
|
|
195
200
|
|
|
196
201
|
- **[Architecture Guide](docs/architecture.md)**: Deep dive into RouteKitAI's design
|
|
197
202
|
- **[Security & Governance](docs/security-and-governance.md)**: Security features and best practices
|
|
198
|
-
- **[
|
|
203
|
+
- **[Full documentation](https://routekitai.readthedocs.io)**: Architecture, security, and guides (Read the Docs)
|
|
199
204
|
|
|
200
205
|
## 🎓 Examples
|
|
201
206
|
|
|
202
207
|
Check out the [`examples/`](examples/) directory for complete examples:
|
|
203
208
|
|
|
204
|
-
- **[Basic Agent](examples/
|
|
205
|
-
- **[
|
|
206
|
-
- **[
|
|
209
|
+
- **[Basic Agent](examples/basic.py)** / **[Hello RouteKit](examples/hello_routekit.py)**: Simple agent with tools
|
|
210
|
+
- **[Real agent with Anthropic](examples/anthropic_agent.py)**: Live Claude agent with tool use (requires `ANTHROPIC_API_KEY`)
|
|
211
|
+
- **[Graph Orchestration](examples/graph_agent.py)** / **[Graph Policy](examples/graph_policy.py)**: Multi-agent workflows
|
|
212
|
+
- **[Supervisor Pattern](examples/supervisor_agent.py)** / **[Supervisor Policy](examples/supervisor_policy.py)**: Supervisor delegating to sub-agents
|
|
213
|
+
- **[ReAct / Plan–Execute / Function Calling](examples/react_policy.py)**, **[Plan–Execute](examples/plan_execute_policy.py)**, **[Function Calling](examples/function_calling_policy.py)**: Policy examples
|
|
207
214
|
- **[Evaluation Harness](examples/eval_regression.py)**: Testing agents with datasets
|
|
208
215
|
|
|
216
|
+
Run all examples: `./scripts/run_examples.sh` (or `bash scripts/run_examples.sh`).
|
|
217
|
+
|
|
209
218
|
## 🛠️ CLI Commands
|
|
210
219
|
|
|
211
|
-
RouteKitAI provides a CLI for common operations:
|
|
220
|
+
RouteKitAI provides a CLI (`routekitai`) for common operations:
|
|
212
221
|
|
|
213
222
|
```bash
|
|
214
223
|
# Run an agent script
|
|
215
|
-
|
|
224
|
+
routekitai run agent_script.py
|
|
216
225
|
|
|
217
226
|
# View a trace (multiple formats available)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
routekitai trace <trace_id> # Table view (default)
|
|
228
|
+
routekitai trace <trace_id> --format timeline # Timeline visualization
|
|
229
|
+
routekitai trace <trace_id> --format steps # Step-by-step execution
|
|
230
|
+
routekitai trace <trace_id> --format json # JSON output
|
|
231
|
+
routekitai trace <trace_id> --format raw # Raw JSONL
|
|
223
232
|
|
|
224
233
|
# Analyze trace metrics
|
|
225
|
-
|
|
234
|
+
routekitai trace-analyze <trace_id> # Performance metrics, token usage, costs
|
|
226
235
|
|
|
227
236
|
# Search traces
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
routekitai trace-search "error" # Search all traces for "error"
|
|
238
|
+
routekitai trace-search "model" --trace-id abc # Search specific trace
|
|
239
|
+
routekitai trace-search "tool" --event-type tool_called # Filter by event type
|
|
231
240
|
|
|
232
241
|
# Replay a trace
|
|
233
|
-
|
|
242
|
+
routekitai replay <trace_id> --agent my_agent
|
|
234
243
|
|
|
235
244
|
# Start web UI for trace visualization
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
routekitai serve # Start on default port 8080
|
|
246
|
+
routekitai serve --port 3000 # Custom port
|
|
247
|
+
routekitai serve --host 0.0.0.0 # Make accessible from network
|
|
239
248
|
|
|
240
249
|
# Run sanity checks
|
|
241
|
-
|
|
250
|
+
routekitai test-agent
|
|
242
251
|
```
|
|
243
252
|
|
|
244
253
|
## 🏗️ Core Primitives
|
|
@@ -271,7 +280,7 @@ pip install -e ".[dev]"
|
|
|
271
280
|
pytest
|
|
272
281
|
|
|
273
282
|
# Run with coverage
|
|
274
|
-
pytest --cov=
|
|
283
|
+
pytest --cov=routekitai --cov-report=html
|
|
275
284
|
|
|
276
285
|
# Run specific test file
|
|
277
286
|
pytest tests/test_runtime.py
|
|
@@ -284,15 +293,15 @@ pytest tests/test_runtime.py
|
|
|
284
293
|
mypy src/
|
|
285
294
|
|
|
286
295
|
# Linting
|
|
287
|
-
ruff check src/
|
|
296
|
+
ruff check src/ tests/ examples/
|
|
288
297
|
|
|
289
298
|
# Format code
|
|
290
|
-
ruff format src/
|
|
299
|
+
ruff format src/ tests/ examples/
|
|
291
300
|
```
|
|
292
301
|
|
|
293
302
|
## 🤝 Contributing
|
|
294
303
|
|
|
295
|
-
Contributions are welcome! Please read
|
|
304
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) and:
|
|
296
305
|
|
|
297
306
|
1. Fork the repository
|
|
298
307
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
@@ -302,8 +311,29 @@ Contributions are welcome! Please read our contributing guidelines (coming soon)
|
|
|
302
311
|
|
|
303
312
|
## 📋 Requirements
|
|
304
313
|
|
|
305
|
-
|
|
306
|
-
|
|
314
|
+
**Core (always installed):**
|
|
315
|
+
|
|
316
|
+
- Python 3.11+
|
|
317
|
+
- [Pydantic](https://docs.pydantic.dev/) 2.x
|
|
318
|
+
- [NumPy](https://numpy.org/) 1.24+
|
|
319
|
+
- [httpx](https://www.python-httpx.org/) 0.24+
|
|
320
|
+
|
|
321
|
+
**Optional extras:**
|
|
322
|
+
|
|
323
|
+
| Extra | Purpose |
|
|
324
|
+
|-------|---------|
|
|
325
|
+
| `[dev]` | CLI, tests, linting, type checking (pytest, mypy, ruff, rich, typer, safety, bandit) |
|
|
326
|
+
| `[ui]` | Web UI for traces: `routekitai serve` (FastAPI, Uvicorn) |
|
|
327
|
+
| `[optional]` | Vector memory, OpenAI adapter (sentence-transformers, faiss, openai, nest-asyncio) |
|
|
328
|
+
| `[docs]` | Build documentation locally (MkDocs, MkDocs Material) |
|
|
329
|
+
|
|
330
|
+
Examples:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
pip install "RouteKitAI[dev]" # development + CLI
|
|
334
|
+
pip install "RouteKitAI[dev,ui]" # + trace web UI
|
|
335
|
+
pip install "RouteKitAI[docs]" # build docs: mkdocs build / mkdocs serve
|
|
336
|
+
```
|
|
307
337
|
|
|
308
338
|
## 📄 License
|
|
309
339
|
|
|
@@ -316,7 +346,7 @@ RouteKitAI is inspired by the need for testable, observable AI agent frameworks.
|
|
|
316
346
|
## 🔗 Links
|
|
317
347
|
|
|
318
348
|
- **GitHub**: [https://github.com/MedGhassen/RouteKitAI](https://github.com/MedGhassen/RouteKitAI)
|
|
319
|
-
- **Documentation**: [https://
|
|
349
|
+
- **Documentation**: [https://routekitai.readthedocs.io](https://routekitai.readthedocs.io)
|
|
320
350
|
- **Issues**: [https://github.com/MedGhassen/RouteKitAI/issues](https://github.com/MedGhassen/RouteKitAI/issues)
|
|
321
351
|
|
|
322
352
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
routekitai/__init__.py,sha256=
|
|
2
|
-
routekitai/message.py,sha256=
|
|
1
|
+
routekitai/__init__.py,sha256=155KUL2u8F-wn5wV7oT4bwzpJ84l2OWhmsU-hk-MM3A,1091
|
|
2
|
+
routekitai/message.py,sha256=owTgRvHctqquWROng2w06c6sMrarKlB6avxHoPhuC3k,830
|
|
3
3
|
routekitai/model.py,sha256=FfGla6Hep-L1bz-rC89leGdbtQed433FZ1yz4eF0wko,1357
|
|
4
4
|
routekitai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
routekitai/tool.py,sha256=o3fOyz_iOyrbYcrc7NYrI7m7JXOLEBZ5WkaKYjsvbF8,1027
|
|
@@ -17,13 +17,13 @@ routekitai/core/agent.py,sha256=z1mkIekJ8bbWebmzNADl3vMQURXDMx82eo2O_GC-TFk,1230
|
|
|
17
17
|
routekitai/core/errors.py,sha256=YN7uo6B7R7KRk5Lwmk4JclJNBQbKOe5YzXLQtvTLKa8,1224
|
|
18
18
|
routekitai/core/hooks.py,sha256=lnp5tK_amFDUbkYWxvWnIDwQ2uI4em2Go0KLb1QtD7c,5644
|
|
19
19
|
routekitai/core/memory.py,sha256=szDTIr3tCgM4aeFO19tOlGj6a8lLPBHTv4KByeUxC2k,1429
|
|
20
|
-
routekitai/core/message.py,sha256
|
|
20
|
+
routekitai/core/message.py,sha256=-nc3rPahQFADkeoA_Ipaq4ELPI14e23JAPIskIvM7js,3471
|
|
21
21
|
routekitai/core/model.py,sha256=vghe66IRXmRO5vNtxQl_L2aVrODE_5Fyj0g_BLCaSMQ,3158
|
|
22
|
-
routekitai/core/policies.py,sha256=
|
|
23
|
-
routekitai/core/policy.py,sha256=
|
|
24
|
-
routekitai/core/policy_adapter.py,sha256=
|
|
25
|
-
routekitai/core/runtime.py,sha256=
|
|
26
|
-
routekitai/core/tool.py,sha256=
|
|
22
|
+
routekitai/core/policies.py,sha256=uNkeArb__aJdNCmdwVa8gWDZVTbSxW-MJHFgBInQFUw,13946
|
|
23
|
+
routekitai/core/policy.py,sha256=CscLNg2Z4U_XV-xC0mf7QlZN-QCJ3zewSz8uPE5NIR8,2814
|
|
24
|
+
routekitai/core/policy_adapter.py,sha256=GeVVQBfd4Bo3Ijzgcq8fdn1CRtC9XOVwb7f4SkfylYY,5887
|
|
25
|
+
routekitai/core/runtime.py,sha256=vOngcUlpJWq2t1ZgdJqQV2gChmlZBQtg9or6Frg3Bkk,61839
|
|
26
|
+
routekitai/core/tool.py,sha256=96zHRidFV8NX9x8fWvbNyT3Up2UL-0BjaNLBl_P79_o,4959
|
|
27
27
|
routekitai/core/tools.py,sha256=9jye7xa1NcJnSARRarRAIMsbF6gmyI4DlyCCv7HuRYM,5215
|
|
28
28
|
routekitai/evals/__init__.py,sha256=9J7kySk46qCOApOlIMvOsPSjc3_NzvxCSuZ8-OejBNU,336
|
|
29
29
|
routekitai/evals/dataset.py,sha256=yivm0NKUjV91OY1jBXdueS64CusHZWnB_f-XyVRTDNA,2512
|
|
@@ -31,7 +31,7 @@ routekitai/evals/metrics.py,sha256=_sSkQxWpXVc_cC8FFzriFyNEQce8aGbx-q5rcqkesEM,3
|
|
|
31
31
|
routekitai/evals/runner.py,sha256=cUpGjfgQy_biq2O1mOf9mZ61jMk6Y6G6Y7Cpv6RZuzQ,6980
|
|
32
32
|
routekitai/graphs/__init__.py,sha256=jCmCWWtV29uDBy47U5OrBwwie6eqMC-uS-AGrXLuonM,274
|
|
33
33
|
routekitai/graphs/executors.py,sha256=1wrZ5mXUMRf6TGbvfddUUm9h8ruyNjMJB6xlZDdRxh0,18329
|
|
34
|
-
routekitai/graphs/graph.py,sha256=
|
|
34
|
+
routekitai/graphs/graph.py,sha256=vX70acCsKWs2vv7ccuUWhqhDVB1fJ-L5tKMVhpFhQLM,6302
|
|
35
35
|
routekitai/memory/__init__.py,sha256=Y4QrSbzzQuutlbfnTqOwBBp6Q0ODtcoVkvXOM6eERqk,343
|
|
36
36
|
routekitai/memory/episodic.py,sha256=iGWpeWo5KikTBHhWZsk41YWuT12JJ139I7Dz54LTdhk,8166
|
|
37
37
|
routekitai/memory/kv.py,sha256=3w883l7MpLmDz3VhX-fzQQXtxpZhElxgCJv3RczgzE8,785
|
|
@@ -47,18 +47,18 @@ routekitai/observability/exporters/__init__.py,sha256=g_YIeoJ_YWANEtETQgF7zEBNtW
|
|
|
47
47
|
routekitai/observability/exporters/base.py,sha256=zhkCem3xXqKmrB38QsFaM1HAbxECPOyLoPuO5Jbd-nA,672
|
|
48
48
|
routekitai/observability/exporters/jsonl.py,sha256=wBHgzfvBSksQQ_WLYXnPF9OH6_a7ldgbTlTUzTs0U7E,2621
|
|
49
49
|
routekitai/observability/exporters/otel.py,sha256=Cl25rA1robwJVaOtoHRT2W3VTGvY3ibJ0eoqXQwbsb8,4249
|
|
50
|
-
routekitai/providers/__init__.py,sha256
|
|
51
|
-
routekitai/providers/anthropic.py,sha256=
|
|
50
|
+
routekitai/providers/__init__.py,sha256=-43vXwysU7NNuluD2QcmNFNmlfKMgOq_5uCnWm6ptyY,277
|
|
51
|
+
routekitai/providers/anthropic.py,sha256=v8aSHz4zvCYW958tV-VjGN5joya8U4LCtSUJ86pNBfs,8634
|
|
52
52
|
routekitai/providers/azure_openai.py,sha256=2iTvaLFKTseKy41E4D1EOT4NvwsxK5BSo5G0xaJjd7Y,8312
|
|
53
53
|
routekitai/providers/local.py,sha256=Ii959ycdAOhnUucAlaBPCNP5YOxgFgRmuCbu_XibQwY,7693
|
|
54
54
|
routekitai/providers/openai.py,sha256=ImTGXOiHyNtXUNF2tzBF4Dkv8r_aPrmQDUH6Cu7-zi4,12641
|
|
55
55
|
routekitai/sandbox/__init__.py,sha256=erkRO6xnq7NcOmv_rV444IRJl0WmhRWoVX1Sx8XiqXg,352
|
|
56
56
|
routekitai/sandbox/filesystem.py,sha256=5bLMhrutoJQVfOM49v1L1ykVpjFee50dERdyY4VaiGM,4744
|
|
57
57
|
routekitai/sandbox/network.py,sha256=cFK--kZ88irTREWgTWhXDBDvynzq9XdHjFn_jGcF3HI,4668
|
|
58
|
-
routekitai/sandbox/permissions.py,sha256=
|
|
59
|
-
routekitai-0.1.
|
|
60
|
-
routekitai-0.1.
|
|
61
|
-
routekitai-0.1.
|
|
62
|
-
routekitai-0.1.
|
|
63
|
-
routekitai-0.1.
|
|
64
|
-
routekitai-0.1.
|
|
58
|
+
routekitai/sandbox/permissions.py,sha256=5rGeF_4JcpetrsVYZuAw-S-rV5vsic1OSJAh0we-uDs,2094
|
|
59
|
+
routekitai-0.2.1.dist-info/licenses/LICENSE,sha256=TftL8ryIHKESApEmr5TBlRg5mengWfrzzjlr7nz46iE,1079
|
|
60
|
+
routekitai-0.2.1.dist-info/METADATA,sha256=goTgbouS7za77-MZnmXGGZWc4bSh_tmAIbzATdBbi24,11747
|
|
61
|
+
routekitai-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
62
|
+
routekitai-0.2.1.dist-info/entry_points.txt,sha256=pHUdSYPaXtmUY5LmHWKcgh2dR-t0DRrgUZ3AO0Pk9L0,56
|
|
63
|
+
routekitai-0.2.1.dist-info/top_level.txt,sha256=lbYswJspf7-baehrsDZboTTxhrabTTPf4mGYC_jXKaY,11
|
|
64
|
+
routekitai-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|