Skip to content

Commit d65c72b

Browse files
Strengthen message/tool validation and standardize ToolFunctionSchema APIs (#109)
* Fix: full message validator * feat(core): add module docstring, runtime check for optimized mode, and refactor agent tool types * Refactor: message validating * Feat: did_you_mean hint
1 parent 73b74a3 commit d65c72b

14 files changed

Lines changed: 348 additions & 79 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "amrita_core"
3-
version = "0.10.3"
3+
version = "0.10.4"
44
description = "High performance, flexible, lightweight agent framework."
55
readme = "README.md"
66
requires-python = ">=3.10,<3.15"

src/amrita_core/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
1+
"""AmritaCore — A lightweight Agent runtime.
2+
3+
AmritaCore is a lightweight Agent runtime built on top of AmritaSense, providing
4+
native async streaming, tool integration, event hooks, and memory management.
5+
This module is the unified entry point for the AmritaCore library, organized
6+
by functional domain:
7+
8+
Core Components
9+
- **Config**: AmritaConfig, get_config, set_config
10+
- **Preset Management**: PresetManager, PresetReport, ModelPreset, ModelConfig
11+
- **Chat Management**: ChatManager, ChatObject, ChatObjectMeta, SuspendEnum
12+
- **Agent Strategy**: AgentStrategy, AgentRuntime, create_agent
13+
14+
Type System
15+
- **Message**: BaseModel, UniResponse, UniResponseUsage
16+
- **Tools**: ToolCall, ToolResult, ToolFunctionSchema, FunctionDefinitionSchema,
17+
FunctionParametersSchema, FunctionPropertySchema, ToolContext, ToolData
18+
- **Content**: TextContent, Function, MemoryModel
19+
20+
Backends & Contexts
21+
- **Backends**: AbilityBackend, MemoryBackend, LegacyBackend, BackendSlots
22+
- **Contexts**: AbilityContext, StateContext
23+
24+
Event Hooks
25+
- **Events**: CompletionEvent, PreCompletionEvent, EventTypeEnum
26+
- **Registration**: on_completion, on_event, on_precompletion
27+
28+
Tool System
29+
- **Management**: ToolsManager, on_tools, simple_tool
30+
- **MCP**: mcp
31+
32+
Chat API
33+
- call_completion, tools_caller, text_generator, get_last_response, get_tokens
34+
35+
Initialization
36+
- load_amrita(): Full initialization with built-in components and MCP clients
37+
- minimal_init(): Minimal initialization
38+
39+
Note: AmritaCore must NOT be run in Python optimized mode (-O / --optimize).
40+
"""
41+
42+
if not __debug__: # fast-fail in optimized mode
43+
raise RuntimeError(
44+
"AmritaCore is running in optimized mode, which will affect functionality. Please run without -O or --optimize flag."
45+
)
146
from amrita_sense.logging import debug_log, logger
247
from amrita_sense.streaming import SuspendObjectStream
348

src/amrita_core/base/adapter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from amrita_sense.logging import logger
99

1010
from amrita_core.config import AmritaConfig, get_config
11+
from amrita_core.utils import _did_you_mean_hint
1112

1213
from ..tools.models import ToolChoice, ToolFunctionSchema
1314
from ..types import EmbeddingChunk, ModelPreset, ToolCall, UniResponse
@@ -120,7 +121,8 @@ def safe_get_adapter(self, protocol: str) -> type[ModelAdapter] | None:
120121
def get_adapter(self, protocol: str) -> type[ModelAdapter]:
121122
"""Get adapter"""
122123
if protocol not in self._adapter_class:
123-
raise ValueError(f"No adapter found for protocol {protocol}")
124+
hint = _did_you_mean_hint(protocol, list(self._adapter_class.keys()))
125+
raise ValueError(f"No adapter found for protocol '{protocol}'.{hint}")
124126
return self._adapter_class[protocol]
125127

126128
def register_adapter(self, adapter: type[ModelAdapter]):

src/amrita_core/base/tokenizer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from amrita_sense.logging import logger
55

6+
from amrita_core.utils import _did_you_mean_hint
7+
68

79
class BaseTokenizer(ABC):
810
__override__: bool = False # Whether to allow overriding existing tokenizers
@@ -89,7 +91,8 @@ def safe_get_tokenizer(self, tok_type: str) -> type[BaseTokenizer] | None:
8991
def get_tokenizer(self, tok_type: str) -> type[BaseTokenizer]:
9092
"""Get tokenizer"""
9193
if tok_type not in self._tokenizer_class:
92-
raise ValueError(f"No tokenizer found for tok_type {tok_type}")
94+
hint = _did_you_mean_hint(tok_type, list(self._tokenizer_class.keys()))
95+
raise ValueError(f"No tokenizer found for tok_type '{tok_type}'.{hint}")
9396
return self._tokenizer_class[tok_type]
9497

9598
def register_tokenizer(self, tokenizer: type[BaseTokenizer]):

src/amrita_core/builtins/agent.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_last_response,
2020
tools_caller,
2121
)
22+
from amrita_core.tools.models import ToolFunctionSchema
2223
from amrita_core.types import (
2324
CONTENT_LIST_TYPE_ITEM,
2425
Function,
@@ -56,6 +57,13 @@
5657
)
5758

5859

60+
def _resolve_tool_name(tool: ToolFunctionSchema | dict) -> str:
61+
"""Resolve the function name from a tool schema (object or dict form)."""
62+
if isinstance(tool, dict):
63+
return tool.get("function", {}).get("name", "")
64+
return tool.function.name
65+
66+
5967
class BaseReActAgentStrategy(AgentStrategy, ABC):
6068
"""
6169
Abstract base class for ReAct agent strategies with common execution logic.
@@ -93,7 +101,7 @@ class BaseReActAgentStrategy(AgentStrategy, ABC):
93101

94102
agent_last_step: str | None = None
95103
call_count = 1
96-
tools: list[Any]
104+
tools: list[ToolFunctionSchema]
97105
origin_msg: str = ""
98106
origin_instruction: str = ""
99107
reasoning_pc = 0
@@ -112,8 +120,8 @@ def __init__(self, ctx: StrategyContext):
112120
self.origin_instruction = self.chat_object.train.content
113121
config = self.chat_object.config
114122
if config.builtin.tool_calling_mode == "agent":
115-
self.tools.append(STOP_TOOL.model_dump())
116-
self.tools.extend(self.tools_manager.tools_meta_dict().values())
123+
self.tools.append(STOP_TOOL)
124+
self.tools.extend(self.tools_manager.tools_meta().values())
117125
# Initialize reasoning enhancement state
118126
self._predicted_tools = []
119127
self.origin_msg: str = (
@@ -131,8 +139,8 @@ async def _generate_reasoning_content(
131139
) -> UniResponse[str, None]:
132140
tools = [
133141
{
134-
"name": tool["function"]["name"],
135-
"description": tool["function"]["description"],
142+
"name": tool.function.name,
143+
"description": tool.function.description,
136144
}
137145
for tool in self.tools
138146
]
@@ -362,7 +370,7 @@ async def _run_reflection(
362370

363371
tool_response = await tools_caller(
364372
msg_list,
365-
[REFLECTION_TOOL.model_dump()],
373+
[REFLECTION_TOOL],
366374
tool_choice=REFLECTION_TOOL,
367375
preset=self.ctx.chat_object.preset,
368376
)
@@ -421,7 +429,7 @@ async def _run_reflection(
421429

422430
async def _generate_reasoning_msg(
423431
self,
424-
tools_ctx: list[dict[str, Any]],
432+
tools_ctx: list[ToolFunctionSchema],
425433
/,
426434
then: Callable[
427435
[
@@ -447,7 +455,7 @@ async def _generate_reasoning_msg(
447455
]
448456
tool_response: UniResponse[None, list[ToolCall] | None] = await tools_caller(
449457
reasoning_trigger_msg,
450-
[REASONING_TOOL.model_dump(), *tools_ctx],
458+
[REASONING_TOOL, *tools_ctx],
451459
tool_choice=REASONING_TOOL,
452460
preset=self.ctx.chat_object.preset,
453461
)
@@ -840,7 +848,9 @@ async def _handle_tool_error_common(
840848
Returns:
841849
Error message string
842850
"""
843-
logger.error(f"Function {function_name} execution failed: {err}")
851+
logger.opt(exception=err, colors=True).error(
852+
f"Function {function_name} execution failed: {err}"
853+
)
844854
config = self.chat_object.config
845855
if (
846856
config.builtin.tool_calling_mode == "agent"
@@ -886,6 +896,7 @@ def get_category(
886896
) -> Literal["workflow"]:
887897
return "workflow"
888898

899+
889900
class HybridReActAgentStrategy(BaseReActAgentStrategy):
890901
"""**Hybrid ReAct Agent Strategy optimized for Mixture of Experts (MoE) architecture models.**
891902
@@ -1087,7 +1098,7 @@ async def single_execute(
10871098
return False
10881099
tools = self.tools.copy()
10891100
if config.builtin.agent_thought_mode.startswith("reasoning"):
1090-
tools.append(REASONING_TOOL.model_dump())
1101+
tools.append(REASONING_TOOL)
10911102

10921103
if (
10931104
self._predicted_tools
@@ -1096,15 +1107,16 @@ async def single_execute(
10961107
and config.builtin.react_config.reasoning_aware_tools
10971108
):
10981109
prioritized = [
1099-
t for t in tools if t["function"]["name"] in self._predicted_tools
1110+
t for t in tools if _resolve_tool_name(t) in self._predicted_tools
11001111
]
11011112
others = [
1102-
t for t in tools if t["function"]["name"] not in self._predicted_tools
1113+
t for t in tools if _resolve_tool_name(t) not in self._predicted_tools
11031114
]
11041115
tools = prioritized + others
11051116
logger.debug(
1106-
f"Reasoning-aware tools: {[t['function']['name'] for t in prioritized]} "
1107-
f"ahead of {len(others)} others"
1117+
f"Reasoning-aware tools:"
1118+
f" {[_resolve_tool_name(t) for t in prioritized]}"
1119+
f" ahead of {len(others)} others"
11081120
)
11091121

11101122
response_msg: UniResponse[None, list[ToolCall] | None] = await tools_caller(
@@ -1320,7 +1332,7 @@ async def single_execute(
13201332
return False
13211333
tools = self.tools.copy()
13221334
if config.builtin.agent_thought_mode.startswith("reasoning"):
1323-
tools.append(REASONING_TOOL.model_dump())
1335+
tools.append(REASONING_TOOL)
13241336

13251337
if (
13261338
self._predicted_tools
@@ -1329,15 +1341,16 @@ async def single_execute(
13291341
and config.builtin.react_config.reasoning_aware_tools
13301342
):
13311343
prioritized = [
1332-
t for t in tools if t["function"]["name"] in self._predicted_tools
1344+
t for t in tools if _resolve_tool_name(t) in self._predicted_tools
13331345
]
13341346
others = [
1335-
t for t in tools if t["function"]["name"] not in self._predicted_tools
1347+
t for t in tools if _resolve_tool_name(t) not in self._predicted_tools
13361348
]
13371349
tools = prioritized + others
13381350
logger.debug(
1339-
f"Reasoning-aware tools: {[t['function']['name'] for t in prioritized]} "
1340-
f"ahead of {len(others)} others"
1351+
f"Reasoning-aware tools:"
1352+
f" {[_resolve_tool_name(t) for t in prioritized]}"
1353+
f" ahead of {len(others)} others"
13411354
)
13421355

13431356
response_msg: UniResponse[None, list[ToolCall] | None] = await tools_caller(

src/amrita_core/builtins/tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
},
7474
required=[
7575
"summary",
76+
"last_step",
7677
],
7778
),
7879
),

0 commit comments

Comments
 (0)