Skip to content

Add DI matcher system, preset fallbacks, and Anthropic adapter#24

Closed
JohnRichard4096 wants to merge 9 commits intomainfrom
feat/matcher
Closed

Add DI matcher system, preset fallbacks, and Anthropic adapter#24
JohnRichard4096 wants to merge 9 commits intomainfrom
feat/matcher

Conversation

@JohnRichard4096
Copy link
Copy Markdown
Member

@JohnRichard4096 JohnRichard4096 commented Feb 26, 2026

close #22

Summary by Sourcery

Add a new dependency-injection-based matcher system with preset fallback handling and Anthropic adapter support, and extend documentation and tests accordingly.

New Features:

  • Introduce a generic dependency injection system for event matchers via Depends and DependsFactory, including concurrent resolution and runtime injection support.
  • Add a preset fallback event (FallbackContext) and on_preset_fallback hook to handle LLM failures with configurable max_fallbacks.
  • Implement an Anthropic/Claude model adapter alongside the existing OpenAI adapter, with CoT think-tag filtering support in the chat pipeline.

Bug Fixes:

  • Adjust ModelConfig and protocol typings and queue generics for better type safety and correct handling of completion content.
  • Fix UniResponse emission in the OpenAI adapter streaming path so the final response is yielded directly.

Enhancements:

  • Refactor the matcher/event registry into a priority-aware MatcherFactory with improved dependency resolution, deprecations, and clearer control flow.
  • Add support for stripping blocks from model output when CoT mode is enabled.
  • Improve list splitting utility typing and remove unused datetime formatting helpers.

Build:

  • Add anthropic and deprecated packages as new project dependencies and bump library version to 0.5.0 in pyproject.toml.

Documentation:

  • Expand English and Chinese guides to document FallbackContext, preset fallback hooks, and the new dependency injection system with examples.
  • Extend API reference navigation and pages to cover Depends, DependsFactory, and FallbackContext classes in both English and Chinese guides.

Tests:

  • Add comprehensive tests for the matcher system, dependency injection behavior, event registry, and integration flows.

@JohnRichard4096
Copy link
Copy Markdown
Member Author

@sourcery-ai title

@JohnRichard4096 JohnRichard4096 deleted the feat/matcher branch February 26, 2026 07:36
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Feb 26, 2026

Reviewer's Guide

Introduces a new event matcher and dependency injection system with support for preset fallback handling, Anthropic adapter integration, and CoT/think-tag filtering; refactors matcher internals around a priority-based EventRegistry and MatcherFactory, adds FallbackContext-based retry flows in ChatObject, extends protocol and config types, and updates docs/tests and project dependencies accordingly.

Class diagram for new matcher and dependency injection system

classDiagram
    class EventRegistry {
        <<singleton>>
        -_instance: EventRegistry
        -_event_handlers: defaultdict~str, defaultdict~int, list~FunctionData~~~~
        +__new__() EventRegistry
        +register_handler(event_type: str, data: FunctionData) void
        +get_handlers(event_type: str) defaultdict~int, list~FunctionData~~~~
        +_all() defaultdict~str, defaultdict~int, list~FunctionData~~~~
        +get_all() defaultdict~str, defaultdict~int, list~FunctionData~~~~
    }

    class FunctionData {
        +function: Callable
        +signature: inspect.Signature
        +frame: FrameType
        +priority: int
        +matcher: Matcher
    }

    class Matcher {
        +event_type: EventTypeEnum~str|
        +priority: int
        +block: bool
        +append_handler(func: Callable[..., Awaitable~Any~]]) void
        +wrap_function(func: Callable[..., Awaitable~Any~]]) Callable
        +set_block(block: bool) void
        +stop_process() void
        +cancel_matcher() void
        +pass_event() void
    }

    class MatcherFactory {
        <<utility>>
        +trigger_event(event: BaseEvent, config: AmritaConfig, *args: Any, exception_ignored: tuple~type[Exception],...~, **kwargs: Any) void
        +_simple_run(matcher_list: list~FunctionData~, event: BaseEvent, config: AmritaConfig, exception_ignored: tuple~type[BaseException],...~, extra_args: tuple, extra_kwargs: dict~str, Any~) bool
        +_resolve_dependencies(signature: inspect.Signature, session_args: Iterable~Any~, session_kwargs: dict~str, Any~) tuple~bool, tuple, dict~str, Any~, dict~str, DependsFactory~~
        +_do_runtime_resolve(runtime_args: dict~int, DependsFactory~, runtime_kwargs: dict~str, DependsFactory~, args2update: list~Any~, kwargs2update: dict~str, Any~, session_args: list~Any~, session_kwargs: dict~str, Any~, exception_ignored: tuple~type[BaseException],...~) bool
    }

    class DependsFactory~T~ {
        -_depency_func: Callable[..., T|Awaitable~T~]
        +__init__(depency: Callable[..., T|Awaitable~T~])
        +resolve(*args, **kwargs) T|None
    }

    class DependsFn {
        <<function>>
        +Depends(dependency: Callable[..., T|Awaitable~T~]) Any
    }

    class BaseEvent {
        <<abstract>>
        +get_event_type() EventTypeEnum~str|
        +event_type: EventTypeEnum~str|
    }

    class EventTypeEnum {
        <<enum>>
        PRE_COMPLETION
        COMPLETION
        PRESET_FALLBACK
        Nil
        +validate(name: str) bool
    }

    class MatcherException {
    }
    class BlockException {
    }
    class CancelException {
    }
    class PassException {
    }

    class AmritaConfig {
    }

    %% Relationships
    EventRegistry --> "*" FunctionData : stores
    FunctionData --> Matcher : matcher
    MatcherFactory ..> EventRegistry : uses
    MatcherFactory ..> FunctionData : runs
    MatcherFactory ..> DependsFactory : resolves

    Matcher ..> EventRegistry : register_handler
    Matcher ..> BaseEvent : handles

    DependsFactory ..> MatcherFactory : uses _resolve_dependencies
    DependsFn ..> DependsFactory : creates

    BaseEvent <|-- FallbackContext
    BaseEvent <|-- Event

    MatcherException <|-- BlockException
    MatcherException <|-- CancelException
    MatcherException <|-- PassException

    MatcherFactory ..> BaseEvent
    MatcherFactory ..> AmritaConfig
    MatcherFactory ..> MatcherException
    MatcherFactory ..> EventTypeEnum

    class FallbackContext {
        +preset: ModelPreset
        +exc_info: BaseException
        +config: AmritaConfig
        +context: SendMessageWrap
        +term: int
        +event_type: EventTypeEnum
        +get_event_type() EventTypeEnum
        +fail(reason: Any|None) Never
    }

    class Event {
        +user_input: USER_INPUT
        +config: AmritaConfig
        +chat_object: ChatObject
        +event_type: EventTypeEnum
        +get_event_type() EventTypeEnum
    }

    class ChatObject {
        +preset: ModelPreset
        +config: AmritaConfig
        +context_wrap: SendMessageWrap
        +_process_chat() None
    }

    ChatObject ..> MatcherFactory : trigger_event
    FallbackContext ..> FallbackFailed
    class FallbackFailed {
    }
Loading

Class diagram for adapters, protocol changes, and CoT filtering

classDiagram
    class LLMConfig {
        +auto_retry: bool
        +max_retries: int
        +max_fallbacks: int
        +enable_memory_abstract: bool
    }

    class ModelConfig {
        +temperature: float
        +top_p: float
        +stream: bool
        +multimodal: bool
        +cot_model: bool
    }

    class ModelPreset {
        +name: str
        +model: str
        +api_key: str
        +base_url: str
        +config: ModelConfig
    }

    class AmritaConfig {
        +llm: LLMConfig
    }

    class UniResponseUsage~T~ {
        +prompt_tokens: T
        +completion_tokens: T
        +total_tokens: T
    }

    class UniResponse {
        +role: str
        +content: str
        +usage: UniResponseUsage~int~
        +tool_calls: Any
    }

    class MessageContent {
    }

    class CompletionChunk {
        +content: str
        +metadata: dict~str, Any~
        +get_content() str
        +get_metadata() dict
    }

    class ModelAdapter {
        <<abstract>>
        -preset: ModelPreset
        -config: AmritaConfig
        +call_api(messages: Iterable, **kwargs) AsyncGenerator~COMPLETION_RETURNING, None~
        +call_tools(...) AsyncGenerator
        +get_adapter_protocol() str|tuple~str,...~
    }

    class OpenAIAdapter {
        +call_api(messages: Iterable, *args, **kwargs) AsyncGenerator~COMPLETION_RETURNING, None~
        +call_tools(...)
    }

    class AnthropicAdapter {
        +call_api(messages: Iterable, *args, **kwargs) AsyncGenerator~COMPLETION_RETURNING, None~
        +get_adapter_protocol() str|tuple~str,...~
    }

    class LibChatInner {
        <<function>>
        +inner(preset: ModelPreset, config: AmritaConfig, messages: Iterable) AsyncGenerator
    }

    ModelAdapter <|-- OpenAIAdapter
    ModelAdapter <|-- AnthropicAdapter

    ModelAdapter --> ModelPreset
    ModelAdapter --> AmritaConfig
    OpenAIAdapter ..> UniResponse
    OpenAIAdapter ..> UniResponseUsage
    AnthropicAdapter ..> UniResponse
    AnthropicAdapter ..> UniResponseUsage

    CompletionChunk --> MessageContent : content type

    LibChatInner ..> ModelAdapter : uses call_api via reflection
    LibChatInner ..> ModelPreset
    LibChatInner ..> AmritaConfig

    note for LibChatInner "If preset.config.cot_model is True, inner() filters out <think>...</think> chunks before yielding to callers"
Loading

File-Level Changes

Change Details Files
Refactor matcher/event system into a priority-aware registry with a factory-based matcher runner and dependency injection support.
  • Replace EventRegistry internal structure with a nested defaultdict keyed by event type and priority, and expose get_handlers/get_all along with a deprecated _all alias.
  • Simplify FunctionData by removing the block flag and strongly typing matcher to Matcher.
  • Introduce DependsFactory and Depends to support declarative and runtime dependency injection, including concurrent resolution and ExceptionGroup aggregation.
  • Add MatcherFactory with _resolve_dependencies, _do_runtime_resolve, _simple_run, and an overloaded trigger_event that performs type-based argument matching, DI resolution, and priority-ordered execution with block/cancel semantics and deprecation of Matcher.cancel_matcher in favor of stop_process.
src/amrita_core/hook/matcher.py
src/amrita_core/hook/on.py
src/amrita_core/hook/exception.py
tests/test_matcher.py
Add preset fallback event flow and error type for LLM request retries, with corresponding event type, hook, and documentation.
  • Extend EventTypeEnum with PRESET_FALLBACK and implement FallbackContext BaseEvent subclass with fail() raising FallbackFailed.
  • Introduce FallbackFailed exception type for failed preset fallback handling.
  • Wire ChatObject._process_chat to perform multi-attempt fallback using FallbackContext, MatcherManager.trigger_event, and a max_fallbacks limit in LLMConfig, raising FallbackFailed when exhausted.
  • Add on_preset_fallback decorator and expand English/Chinese event concept docs and API references to describe FallbackContext and the new hook.
src/amrita_core/hook/event.py
src/amrita_core/hook/on.py
src/amrita_core/hook/exception.py
src/amrita_core/chatmanager.py
src/amrita_core/config.py
docs/guide/concepts/event.md
docs/zh/guide/concepts/event.md
docs/guide/api-reference/classes/FallbackContext.md
docs/zh/guide/api-reference/classes/FallbackContext.md
docs/.vitepress/config.mts
Introduce DI-related public API surface (Depends/DependsFactory) and documentation, and test coverage for matcher/DI behavior.
  • Expose Depends and DependsFactory from matcher module for external use and document them in English and Chinese API reference, including their behavior and best practices.
  • Add comprehensive tests for DependsFactory, Depends, EventRegistry, Matcher decorators, MatcherFactory._do_runtime_resolve/_simple_run, trigger_event, and integration scenarios including concurrent dependency resolution.
src/amrita_core/hook/matcher.py
docs/guide/api-reference/index.md
docs/zh/guide/api-reference/index.md
docs/guide/api-reference/classes/Depends.md
docs/zh/guide/api-reference/classes/Depends.md
docs/guide/api-reference/classes/DependsFactory.md
docs/zh/guide/api-reference/classes/DependsFactory.md
tests/test_matcher.py
Extend model adapter layer with Anthropic protocol support, adjust protocol interface, and update OpenAI adapter behavior.
  • Add AnthropicAdapter implementing ModelAdapter.call_api for Anthropic/Claude-compatible endpoints, including streaming and non-streaming handling, usage accounting, and protocol identification via get_adapter_protocol.
  • Introduce a helper model_dump() to normalize message payloads from Pydantic models or dicts before sending to Anthropic.
  • Change ModelAdapter.call_api abstract signature to accept **kwargs, and adapt OpenAIAdapter.call_api signature accordingly while simplifying its final UniResponse yield pattern.
src/amrita_core/builtins/adapter.py
src/amrita_core/protocol.py
pyproject.toml
Enhance CoT handling, utilities typing, and minor type/test cleanups.
  • Filter out <think>/</think> segments in libchat.get_completion when cot_model is enabled in ModelConfig, only yielding visible content to callers.
  • Add cot_model flag to ModelConfig for controlling think-tag stripping behavior.
  • Make split_list generic and adjust return type to list[list[T]]; remove unused timestamp formatting utilities and tests.
  • Tighten various type hints (e.g., Queue item type, ModelPreset-related aliases, ProtocolMessage.get_content return type) and adjust tests/types accordingly.
src/amrita_core/libchat.py
src/amrita_core/types.py
src/amrita_core/utils.py
src/amrita_core/chatmanager.py
src/amrita_core/protocol.py
tests/test_utils.py
src/amrita_core/builtins/adapter.py

Assessment against linked issues

Issue Objective Addressed Explanation
#22 Add an on_model_fallback decorator (mirroring on_completion/on_precompletion) that allows registering async generator handlers for model-call failures, using a FallbackContext carrying session_id, config, original_context, error, chat_object, and preset. The PR introduces on_preset_fallback instead of on_model_fallback, and the handlers are standard async functions (no explicit async generator pattern for yielding responses). The FallbackContext added in hook/event.py contains fields preset, exc_info, config, context, and term, but does not include session_id or chat_object, nor exactly the original_context field as specified. The decorator interface and context structure differ from the issue’s requested design.
#22 Integrate the fallback hook into the model calling pipeline (e.g., in libchat.py / model call path) so that when tools or direct model calls fail, the fallback handlers are triggered and can participate in the async generator response flow. The PR integrates a preset-fallback mechanism in ChatObject._process_chat within chatmanager.py, not directly in libchat.py where call_completion is defined. On exceptions during call_completion, it creates a FallbackContext and calls MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,)), allowing handlers to modify the preset or fail. However, this mechanism is focused on preset retry logic, not on yielding fallback responses in the same async generator flow as model outputs. The requested behavior of using async generator fallback handlers to provide fallback responses is not implemented.
#22 Document the new model-fallback hook and context type in the guides/API reference, consistent with the event-driven architecture used for other hooks. The PR adds documentation for FallbackContext and on_preset_fallback in both English and Chinese guides and API reference, but these docs describe a preset fallback event (changing presets on failure), not the requested on_model_fallback hook that yields fallback responses. Thus, while a related feature is documented, the specific on_model_fallback hook described in the issue is not documented or exposed.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot changed the title Feat/matcher Add DI matcher system, preset fallbacks, and Anthropic adapter Feb 26, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In MatcherFactory._simple_run, isinstance(e, CancelException | BlockException) will raise a TypeError on Python 3.10+ because | produces a types.UnionType; use isinstance(e, (CancelException, BlockException)) instead.
  • In ChatObject._process_chat, the fallback logging and trigger call look inconsistent with the new max_fallbacks behavior: the warning still uses self.config.llm.max_retries in the message, and MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,)) passes FallbackFailed as a positional arg rather than via exception_ignored=(FallbackFailed,), so fallback exceptions won’t be treated as intended.
  • The helper model_dump in builtins/adapter.py shadows its obj parameter name inside the list comprehension (for obj in obj), which is confusing and easy to misuse; consider renaming the loop variable (e.g. for item in obj) to avoid shadowing and clarify intent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MatcherFactory._simple_run`, `isinstance(e, CancelException | BlockException)` will raise a `TypeError` on Python 3.10+ because `|` produces a `types.UnionType`; use `isinstance(e, (CancelException, BlockException))` instead.
- In `ChatObject._process_chat`, the fallback logging and trigger call look inconsistent with the new `max_fallbacks` behavior: the warning still uses `self.config.llm.max_retries` in the message, and `MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,))` passes `FallbackFailed` as a positional arg rather than via `exception_ignored=(FallbackFailed,)`, so fallback exceptions won’t be treated as intended.
- The helper `model_dump` in `builtins/adapter.py` shadows its `obj` parameter name inside the list comprehension (`for obj in obj`), which is confusing and easy to misuse; consider renaming the loop variable (e.g. `for item in obj`) to avoid shadowing and clarify intent.

## Individual Comments

### Comment 1
<location path="src/amrita_core/hook/matcher.py" line_range="448-449" />
<code_context>
+                    elif isinstance(chunk, MessageContent | str):
+                        await self.yield_response(chunk)
+                break
+            except Exception as e:
+                logger.warning(
+                    f"Because of `{e!s}`, LLM request failed, retrying ({i}/{self.config.llm.max_retries})..."
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `CancelException | BlockException` inside `isinstance` will raise a `TypeError` at runtime.

This will raise `TypeError: isinstance() argument 2 cannot be a union` at runtime, since `isinstance` expects a type or tuple of types, not a PEP 604 union. Use a tuple instead, e.g.:

```python
if isinstance(e, (CancelException, BlockException)):
    logger.info("Cancelled Matcher processing")
    return False
```

As written, the `TypeError` will mask the original exception and break the intended control flow.
</issue_to_address>

### Comment 2
<location path="docs/guide/api-reference/classes/DependsFactory.md" line_range="8" />
<code_context>
+## Constructor
+
+```python
+def __init__(self, depency: Callable[..., T | Awaitable[T]])
+```
+
</code_context>
<issue_to_address>
**issue (typo):** Typo in parameter name `depency` – should be `dependency`.

Please rename the parameter to `dependency` in the constructor (and corresponding docs) to keep the public API clear and consistent.
</issue_to_address>

### Comment 3
<location path="docs/zh/guide/api-reference/classes/DependsFactory.md" line_range="8" />
<code_context>
+## Constructor
+
+```python
+def __init__(self, depency: Callable[..., T | Awaitable[T]])
+```
+
</code_context>
<issue_to_address>
**issue (typo):** 参数名 `depency` 存在拼写错误,建议改为 `dependency`。

该拼写在构造函数签名及后续参数说明中也一致使用了 `depency`,请统一更正为 `dependency`。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +448 to +449
except Exception as e:
if isinstance(e, CancelException | BlockException):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using CancelException | BlockException inside isinstance will raise a TypeError at runtime.

This will raise TypeError: isinstance() argument 2 cannot be a union at runtime, since isinstance expects a type or tuple of types, not a PEP 604 union. Use a tuple instead, e.g.:

if isinstance(e, (CancelException, BlockException)):
    logger.info("Cancelled Matcher processing")
    return False

As written, the TypeError will mask the original exception and break the intended control flow.

## Constructor

```python
def __init__(self, depency: Callable[..., T | Awaitable[T]])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): Typo in parameter name depency – should be dependency.

Please rename the parameter to dependency in the constructor (and corresponding docs) to keep the public API clear and consistent.

## Constructor

```python
def __init__(self, depency: Callable[..., T | Awaitable[T]])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): 参数名 depency 存在拼写错误,建议改为 dependency

该拼写在构造函数签名及后续参数说明中也一致使用了 depency,请统一更正为 dependency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[New Feature] Add on_model_fallback or similiar hook.

1 participant