From 2763364c76d40ea93a207d6f38ef806beea1596a Mon Sep 17 00:00:00 2001 From: John Richard Date: Sat, 25 Apr 2026 20:07:38 +0800 Subject: [PATCH 1/5] Feat: non-config event;Quality: Libchat --- pyproject.toml | 2 +- src/amrita_core/builtins/agent.py | 46 ++++++++------- src/amrita_core/hook/exception.py | 5 -- src/amrita_core/hook/matcher.py | 93 +++++++++++-------------------- src/amrita_core/libchat.py | 45 ++++++++++----- src/amrita_core/tools/mcp.py | 5 -- tests/test_libchat.py | 32 ----------- 7 files changed, 93 insertions(+), 135 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 173ea95..5883371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "amrita_core" -version = "0.8.3.1" +version = "0.8.4" description = "High performance, lightweight agent framework." readme = "README.md" requires-python = ">=3.10,<3.15" diff --git a/src/amrita_core/builtins/agent.py b/src/amrita_core/builtins/agent.py index c17c243..fc9103b 100644 --- a/src/amrita_core/builtins/agent.py +++ b/src/amrita_core/builtins/agent.py @@ -174,8 +174,8 @@ async def _generate_reasoning_msg( then: Callable[ [ Self, - ToolCall, # trigger_response - UniResponse[str, None], # tool_response + ToolCall, + UniResponse[str, None], ], Awaitable[Any], ], @@ -303,21 +303,16 @@ async def _build_stop_response_and_append( pass @abstractmethod - async def _append_tool_result_to_context( - self, - tool_call: ToolCall, - func_response: str, - response_msg: UniResponse[None, list[ToolCall] | None], + async def _append_reasoning( + self, tool_call: ToolCall, reasoning_content: UniResponse[str, None] ): - """Append tool result to context (strategy-specific). + """Append reasoning content to context (strategy-specific). - Subclasses must implement this to define how tool results are added to context. - Subclasses should use self.ctx.message to access the message list. + Subclasses must implement this to define how reasoning results are added to context. Args: - tool_call: The tool call object - func_response: The function execution result - response_msg: The original response message + tool_call: The tool call object containing the reasoning request + reasoning_content: The response containing the generated reasoning content """ ... @@ -453,15 +448,21 @@ async def _handle_error_append( ... @abstractmethod - async def _append_reasoning( - self, tool_call: ToolCall, reasoning_content: UniResponse[str, None] + async def _append_tool_result_to_context( + self, + tool_call: ToolCall, + func_response: str, + response_msg: UniResponse[None, list[ToolCall] | None], ): - """Append reasoning content to context (strategy-specific). + """Append tool result to context (strategy-specific). - Subclasses must implement this to define how reasoning results are added to context. + Subclasses must implement this to define how tool results are added to context. + Subclasses should use self.ctx.message to access the message list. Args: - response: The response from tools_caller containing reasoning tool calls + tool_call: The tool call object + func_response: The function execution result + response_msg: The original response message """ ... @@ -942,4 +943,11 @@ def get_category(cls) -> Literal["agent-mixed"]: AmritaAgentStrategy = ReActAgentStrategy # Alias for backward compatibility -__all__ = ["PROCESS_MESSAGE"] # backward compatibility +__all__ = [ + "PROCESS_MESSAGE", + "AmritaAgentStrategy", + "BaseReActAgentStrategy", + "HybridReActAgentStrategy", + "NoActionAgentStrategy", + "ReActAgentStrategy", +] # backward compatibility diff --git a/src/amrita_core/hook/exception.py b/src/amrita_core/hook/exception.py index 75c140b..1b132ca 100644 --- a/src/amrita_core/hook/exception.py +++ b/src/amrita_core/hook/exception.py @@ -1,11 +1,6 @@ class MatcherException(Exception): """Base exception for Matcher.""" - -class BlockException(MatcherException): - pass - - class CancelException(MatcherException): pass diff --git a/src/amrita_core/hook/matcher.py b/src/amrita_core/hook/matcher.py index a72af1c..95cc147 100644 --- a/src/amrita_core/hook/matcher.py +++ b/src/amrita_core/hook/matcher.py @@ -25,7 +25,6 @@ from .event import BaseEvent from .exception import ( - BlockException, CancelException, MatcherException, PassException, @@ -259,6 +258,7 @@ def _resolve_dependencies( @staticmethod async def _do_runtime_resolve( + *, runtime_args: dict[int, DependsFactory], runtime_kwargs: dict[str, DependsFactory], args2update: list[Any], @@ -327,17 +327,18 @@ async def _simple_run( cls, matcher_list: list[FunctionData], event: BaseEvent, - config: AmritaConfig, + /, exception_ignored: tuple[type[BaseException], ...], extra_args: tuple, extra_kwargs: dict[str, Any], + config: AmritaConfig | None = None, ) -> bool: """Run a round of matcher Args: matcher_list (list[FunctionData]): Matchers to run event (BaseEvent): event - config (AmritaConfig): Config + config (AmritaConfig, optional): Config exception_ignored (tuple[type[BaseException], ...]): Exceptions to ignore(to raise again) extra_args (tuple): extra args for dependency injection extra_kwargs (dict[str, Any]): extra kwargs for dependency injection @@ -346,12 +347,14 @@ async def _simple_run( bool: Should continue to run. """ for func in matcher_list: - signature = func.signature - frame = func.frame - line_number = frame.f_lineno - file_name = frame.f_code.co_filename + signature: inspect.Signature = func.signature + frame: FrameType = func.frame + line_number: int = frame.f_lineno + file_name: str = frame.f_code.co_filename handler = func.function - session_args = [func.matcher, event, config, *extra_args] + session_args = [func.matcher, event, *extra_args] + ( + [config] if config else [] + ) session_kwargs: dict[str, Any] = deepcopy(extra_kwargs) runtime_args: dict[int, DependsFactory] = { # index -> DependsFactory k: v @@ -364,13 +367,13 @@ async def _simple_run( # These args/kwargs will be generated by Depends if runtime_args or runtime_kwargs: if not await cls._do_runtime_resolve( - runtime_args, - runtime_kwargs, - session_args, - session_kwargs, - session_args, - session_kwargs, - exception_ignored, + runtime_args=runtime_args, + runtime_kwargs=runtime_kwargs, + args2update=session_args, + kwargs2update=session_kwargs, + session_args=session_args, + session_kwargs=session_kwargs, + exception_ignored=exception_ignored, ): raise RuntimeError("Runtime arguments cannot be resolved") @@ -393,13 +396,13 @@ async def _simple_run( continue # Do kwargs dependency injection if d_kw and not await cls._do_runtime_resolve( - {}, - d_kw, - [], - f_kwargs, - session_args, - session_kwargs, - exception_ignored, + runtime_args={}, + runtime_kwargs=d_kw, + args2update=[], + kwargs2update=f_kwargs, + session_args=session_args, + session_kwargs=session_kwargs, + exception_ignored=exception_ignored, ): continue @@ -414,7 +417,7 @@ async def _simple_run( ) continue except Exception as e: - if isinstance(e, CancelException | BlockException): + if isinstance(e, CancelException): logger.info("Cancelled Matcher processing") return False elif isinstance(e, ChatException): @@ -437,16 +440,8 @@ async def trigger_event( cls, event: BaseEvent, config: AmritaConfig, - /, + *args: Any, exception_ignored: tuple[type[Exception], ...] = (), - ) -> None: ... - - @overload - @classmethod - async def trigger_event( - cls, - event: BaseEvent, - config: AmritaConfig, **kwargs: Any, ) -> None: ... @@ -455,24 +450,6 @@ async def trigger_event( async def trigger_event( cls, event: BaseEvent, - config: AmritaConfig, - *args: Any, - ) -> None: ... - @overload - @classmethod - async def trigger_event( - cls, - event: BaseEvent, - config: AmritaConfig, - *args: Any, - exception_ignored: tuple[type[Exception], ...] = (), - ) -> None: ... - @overload - @classmethod - async def trigger_event( - cls, - event: BaseEvent, - config: AmritaConfig, *args: Any, exception_ignored: tuple[type[Exception], ...] = (), **kwargs: Any, @@ -482,16 +459,17 @@ async def trigger_event( async def trigger_event( cls, event: BaseEvent, - config: AmritaConfig, *args: Any, + config: None = None, + exception_ignored: tuple[type[Exception], ...] = (), **kwargs: Any, ) -> None: ... @classmethod async def trigger_event( cls, event: BaseEvent, - config: AmritaConfig, *args: Any, + config: AmritaConfig | None = None, exception_ignored: tuple[type[Exception], ...] = (), **kwargs, ) -> None: @@ -514,9 +492,6 @@ async def trigger_event( config = i if not event: raise RuntimeError("No event found in args") - elif not config: - raise RuntimeError("No config found in args") - session_kwargs = kwargs event_type: EventTypeEnum | str = event.get_event_type() # Get event type handlers = EventRegistry().get_handlers(event_type) @@ -529,10 +504,10 @@ async def trigger_event( if not await cls._simple_run( handlers[priority], event, - config, - exception_ignored, - args, - session_kwargs, + exception_ignored=exception_ignored, + extra_args=args, + extra_kwargs=session_kwargs, + config=config, ): break else: diff --git a/src/amrita_core/libchat.py b/src/amrita_core/libchat.py index ea9b178..96f4c49 100644 --- a/src/amrita_core/libchat.py +++ b/src/amrita_core/libchat.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from collections.abc import AsyncGenerator, Callable, Generator, Sequence +from collections.abc import AsyncGenerator, Callable, Generator, Iterable, Sequence from io import StringIO from pydantic import ValidationError @@ -22,6 +22,7 @@ from .types import ( CONTENT_LIST_TYPE, CONTENT_LIST_TYPE_ITEM, + EmbeddingChunk, Message, ModelPreset, ToolCall, @@ -157,10 +158,8 @@ def _validate_msg_list( async def _call_with_reflection( preset: ModelPreset, - call_func: typing.Callable[..., typing.Awaitable[T]], + call_func: typing.Callable[[ModelAdapter], typing.Awaitable[T]], config: AmritaConfig, - *args, - **kwargs, ) -> T: """Internal helper to call an adapter function with reflection and logging. @@ -168,8 +167,6 @@ async def _call_with_reflection( preset: Model preset to use for the call call_func: Async function to call on the adapter config: Configuration to pass to the adapter - *args: Arguments to pass to the call function - **kwargs: Keyword arguments to pass to the call function Returns: Result of the call function @@ -195,7 +192,7 @@ async def _call_with_reflection( debug_log(f"API URL: {preset.base_url}") debug_log(f"Model: {preset.model}") adapter = adapter_class(preset, config) - return await call_func(adapter, *args, **kwargs) + return await call_func(adapter) async def tools_caller( @@ -221,15 +218,14 @@ async def tools_caller( async def _call_tools( adapter: ModelAdapter, - messages: CONTENT_LIST_TYPE, - tools, - tool_choice, ): return await adapter.call_tools(messages, tools, tool_choice) preset = preset or PresetManager().get_default_preset() return await _call_with_reflection( - preset, _call_tools, config, messages, tools, tool_choice + preset, + _call_tools, + config, ) @@ -237,6 +233,7 @@ async def call_completion( messages: CONTENT_LIST_TYPE, preset: ModelPreset | None = None, config: AmritaConfig | None = None, + **kwargs, ) -> AsyncGenerator[COMPLETION_RETURNING, None]: """Get chat response from the model. @@ -253,14 +250,14 @@ async def call_completion( config = config or get_config() async def _call_api( - adapter: ModelAdapter, messages: CONTENT_LIST_TYPE + adapter: ModelAdapter, ) -> Callable[ [], AsyncGenerator[MessageContent | str | UniResponse[str, None], typing.Any] ]: - return lambda: adapter.call_api([(i.model_dump()) for i in messages]) + return lambda: adapter.call_api([(i.model_dump()) for i in messages], **kwargs) # Call adapter to get chat response - response = await _call_with_reflection(preset, _call_api, config, messages) + response = await _call_with_reflection(preset, _call_api, config) is_thinking = False async for resp in response(): if preset.config.cot_model: @@ -303,3 +300,23 @@ async def get_last_response( if resp is None: raise RuntimeError("No response found in generator.") return resp + + +async def call_embedding( + text: Iterable[str], + preset: ModelPreset, + config: AmritaConfig | None = None, + **kwargs, +) -> Sequence[EmbeddingChunk]: + config = config or get_config() + + async def _call_embed( + adapter: ModelAdapter, + ) -> Sequence[EmbeddingChunk]: + return await adapter.call_embed(text, **kwargs) + + return await _call_with_reflection( + preset, + _call_embed, + config, + ) diff --git a/src/amrita_core/tools/mcp.py b/src/amrita_core/tools/mcp.py index 1f7b3e9..eb0ddb2 100644 --- a/src/amrita_core/tools/mcp.py +++ b/src/amrita_core/tools/mcp.py @@ -32,11 +32,6 @@ str | Path # TODO: Support all types of scripts ) - -class NOT_GIVEN: - pass - - class MCPClient: """Reusable MCP Client""" diff --git a/tests/test_libchat.py b/tests/test_libchat.py index 3115360..c20c047 100644 --- a/tests/test_libchat.py +++ b/tests/test_libchat.py @@ -145,38 +145,6 @@ def mock_adapter_class(self): mock_adapter_instance.some_method = AsyncMock(return_value="test_result") mock_adapter.get_type.return_value = "text-gen" return mock_adapter - - @pytest.mark.asyncio - async def test_call_with_reflection_success(self, mock_adapter_class): - """Test successful call with reflection""" - from amrita_core.config import AmritaConfig - from amrita_core.types import ModelPreset - - # Mock the adapter manager - with patch.object( - AdapterManager, "safe_get_adapter", return_value=mock_adapter_class - ): - preset = ModelPreset( - model="test-model", - name="test-preset", - api_key="test-key", - protocol="test-protocol", - ) - config = AmritaConfig() - - async def test_call_func(adapter, *args, **kwargs): - return await adapter.some_method(*args, **kwargs) - - result = await _call_with_reflection( - preset, test_call_func, config, "arg1", kwarg1="value1" - ) - - assert result == "test_result" - mock_adapter_class.assert_called_once_with(preset, config) - mock_adapter_class.return_value.some_method.assert_called_once_with( - "arg1", kwarg1="value1" - ) - @pytest.mark.asyncio async def test_call_with_reflection_undefined_protocol(self): """Test call with undefined protocol""" From 4f21e4ed51e74b95db835fea30d3594d782a190f Mon Sep 17 00:00:00 2001 From: John Richard Date: Sat, 25 Apr 2026 20:10:54 +0800 Subject: [PATCH 2/5] Fix: type hint --- src/amrita_core/hook/matcher.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/amrita_core/hook/matcher.py b/src/amrita_core/hook/matcher.py index 95cc147..664b96a 100644 --- a/src/amrita_core/hook/matcher.py +++ b/src/amrita_core/hook/matcher.py @@ -441,7 +441,7 @@ async def trigger_event( event: BaseEvent, config: AmritaConfig, *args: Any, - exception_ignored: tuple[type[Exception], ...] = (), + exception_ignored: tuple[type[BaseException], ...] = (), **kwargs: Any, ) -> None: ... @@ -451,7 +451,7 @@ async def trigger_event( cls, event: BaseEvent, *args: Any, - exception_ignored: tuple[type[Exception], ...] = (), + exception_ignored: tuple[type[BaseException], ...] = (), **kwargs: Any, ) -> None: ... @overload @@ -461,7 +461,7 @@ async def trigger_event( event: BaseEvent, *args: Any, config: None = None, - exception_ignored: tuple[type[Exception], ...] = (), + exception_ignored: tuple[type[BaseException], ...] = (), **kwargs: Any, ) -> None: ... @classmethod @@ -470,7 +470,7 @@ async def trigger_event( event: BaseEvent, *args: Any, config: AmritaConfig | None = None, - exception_ignored: tuple[type[Exception], ...] = (), + exception_ignored: tuple[type[BaseException], ...] = (), **kwargs, ) -> None: """Trigger a specific type of event and call all registered event handlers for that type. From f517c8d960ca49a3090b1047222303c89f5144d9 Mon Sep 17 00:00:00 2001 From: John Richard Date: Sat, 25 Apr 2026 20:15:09 +0800 Subject: [PATCH 3/5] Fix: Unit tests --- src/amrita_core/hook/matcher.py | 10 +++++++++- tests/test_matcher.py | 20 +++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/amrita_core/hook/matcher.py b/src/amrita_core/hook/matcher.py index 664b96a..ae35bf0 100644 --- a/src/amrita_core/hook/matcher.py +++ b/src/amrita_core/hook/matcher.py @@ -258,7 +258,6 @@ def _resolve_dependencies( @staticmethod async def _do_runtime_resolve( - *, runtime_args: dict[int, DependsFactory], runtime_kwargs: dict[str, DependsFactory], args2update: list[Any], @@ -464,6 +463,15 @@ async def trigger_event( exception_ignored: tuple[type[BaseException], ...] = (), **kwargs: Any, ) -> None: ... + + @overload + @classmethod + async def trigger_event( + cls, + event: BaseEvent, + *args: Any, + **kwargs: Any, + ) -> None: ... @classmethod async def trigger_event( cls, diff --git a/tests/test_matcher.py b/tests/test_matcher.py index a62a26b..10d3c31 100644 --- a/tests/test_matcher.py +++ b/tests/test_matcher.py @@ -367,7 +367,7 @@ async def test_handler(event: TestEvent, config: AmritaConfig): handlers = EventRegistry().get_handlers("test_event") result = await MatcherFactory._simple_run( - handlers[matcher.priority], event, self.config, (), (), {} + handlers[matcher.priority], event, (), (), {}, self.config ) assert result is True @@ -396,10 +396,10 @@ async def test_handler( result = await MatcherFactory._simple_run( handlers[matcher.priority], event, - self.config, (), (), {"runtime_dep": Depends(dep_func)}, + self.config, ) assert result is True @@ -428,7 +428,12 @@ async def test_handler( handlers = EventRegistry().get_handlers("test_event") result = await MatcherFactory._simple_run( - handlers[matcher.priority], event, self.config, (), (), {} + handlers[matcher.priority], + event, + (), + (), + {}, + self.config, ) assert result is True @@ -454,10 +459,10 @@ async def test_handler(event: TestEvent, config: AmritaConfig, bad_dep: str): await MatcherFactory._simple_run( handlers[matcher.priority], event, - self.config, (), (), {"bad_dep": Depends(failing_dep)}, + self.config, ) @pytest.mark.asyncio @@ -481,7 +486,12 @@ async def test_handler( handlers = EventRegistry().get_handlers("test_event") result = await MatcherFactory._simple_run( - handlers[matcher.priority], event, self.config, (), (), {} + handlers[matcher.priority], + event, + (), + (), + {}, + self.config, ) assert result is True # Should continue to next handler From 8d30a7d2b0d2d7d8ac0229329dee37da1ad481fa Mon Sep 17 00:00:00 2001 From: John Richard Date: Sat, 25 Apr 2026 20:15:26 +0800 Subject: [PATCH 4/5] Style: format code --- src/amrita_core/hook/exception.py | 1 + src/amrita_core/tools/mcp.py | 1 + tests/test_libchat.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/amrita_core/hook/exception.py b/src/amrita_core/hook/exception.py index 1b132ca..cc830ff 100644 --- a/src/amrita_core/hook/exception.py +++ b/src/amrita_core/hook/exception.py @@ -1,6 +1,7 @@ class MatcherException(Exception): """Base exception for Matcher.""" + class CancelException(MatcherException): pass diff --git a/src/amrita_core/tools/mcp.py b/src/amrita_core/tools/mcp.py index eb0ddb2..2b36777 100644 --- a/src/amrita_core/tools/mcp.py +++ b/src/amrita_core/tools/mcp.py @@ -32,6 +32,7 @@ str | Path # TODO: Support all types of scripts ) + class MCPClient: """Reusable MCP Client""" diff --git a/tests/test_libchat.py b/tests/test_libchat.py index c20c047..b916908 100644 --- a/tests/test_libchat.py +++ b/tests/test_libchat.py @@ -145,6 +145,7 @@ def mock_adapter_class(self): mock_adapter_instance.some_method = AsyncMock(return_value="test_result") mock_adapter.get_type.return_value = "text-gen" return mock_adapter + @pytest.mark.asyncio async def test_call_with_reflection_undefined_protocol(self): """Test call with undefined protocol""" From 844b5182012559956251899d75de4bf682fdbbf0 Mon Sep 17 00:00:00 2001 From: John Richard Date: Sat, 25 Apr 2026 20:16:16 +0800 Subject: [PATCH 5/5] Remove: unused tests --- tests/test_mcp.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 79c8594..dbd4417 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -4,7 +4,6 @@ import pytest from amrita_core.tools.mcp import ( - NOT_GIVEN, ClientManager, MCPClient, MultiClientManager, @@ -188,9 +187,3 @@ def test_inheritance_from_multi_client_manager(self): manager = ClientManager() assert isinstance(manager, MultiClientManager) - - -def test_not_given(): - # Test NOT_GIVEN class - assert NOT_GIVEN is not None - assert isinstance(NOT_GIVEN, type)