Skip to content

Feat/matcher and fallbacks#25

Merged
JohnRichard4096 merged 9 commits intomainfrom
feat/matcher-and-fallbacks
Feb 26, 2026
Merged

Feat/matcher and fallbacks#25
JohnRichard4096 merged 9 commits intomainfrom
feat/matcher-and-fallbacks

Conversation

@JohnRichard4096
Copy link
Copy Markdown
Member

@JohnRichard4096 JohnRichard4096 commented Feb 26, 2026

close #22

Summary by Sourcery

Introduce a dependency injection–aware matcher/event system with preset fallback handling, Anthropic adapter support, and chain-of-thought filtering, while updating configuration and docs accordingly.

New Features:

  • Add a Depends/DependsFactory-based dependency injection system for event matchers, including runtime and default dependency resolution.
  • Introduce FallbackContext events and on_preset_fallback hooks to support configurable preset fallback logic on LLM request failures.
  • Add an Anthropic/Claude model adapter alongside the existing OpenAI adapter, including protocol registration and streaming support.
  • Support chain-of-thought style models by optionally filtering out ... segments based on model configuration.

Bug Fixes:

  • Tighten typing and return types in various helpers and protocols, including split_list, MessageOutput.get_content, and queue types in ChatObject.
  • Fix OpenAI adapter response handling to yield the unified response directly from the streaming loop.

Enhancements:

  • Refactor MatcherManager into MatcherFactory with priority-grouped handler storage, richer exception handling, and more flexible trigger_event overloads.
  • Extend ModelConfig and LLMConfig to support COT models and configurable maximum preset fallbacks.
  • Improve event and matcher exceptions by unifying them under MatcherException and adding a dedicated FallbackFailed exception.
  • Remove unused datetime formatting utilities now superseded by other logic.

Build:

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

Documentation:

  • Expand English and Chinese event concept guides with preset fallback handling, the new on_preset_fallback hook, and the dependency injection system.
  • Add API reference docs for Depends, DependsFactory, and FallbackContext in both English and Chinese, and link them from the navigation menus.
  • Clarify and reorder sections in the event guides to cover new hooks and parameter injection semantics.

Tests:

  • Add comprehensive unit and integration tests for the matcher, dependency injection, and event triggering behavior, including concurrency, error handling, and decorator registration.

@JohnRichard4096 JohnRichard4096 merged commit 8cb1602 into main Feb 26, 2026
4 checks passed
@JohnRichard4096 JohnRichard4096 deleted the feat/matcher-and-fallbacks branch February 26, 2026 07:44
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Feb 26, 2026

Reviewer's Guide

Refactors the hook matcher system to support a generic dependency injection mechanism (Depends/DependsFactory), priority-based handler registries, and new preset fallback events, while adding an Anthropic adapter, CoT thinking-filtering, preset fallback retries, and accompanying docs/tests and config updates.

Sequence diagram for triggering events with dependency injection

sequenceDiagram
    actor User
    participant ChatObject
    participant MatcherManager
    participant EventRegistry
    participant DependsFactory
    participant Handler as EventHandler

    User->>ChatObject: create ChatObject(hook_args, hook_kwargs)
    User->>ChatObject: send_message()
    ChatObject->>ChatObject: build BaseEvent
    ChatObject->>MatcherManager: trigger_event(event, config, *hook_args, **hook_kwargs)

    activate MatcherManager
    MatcherManager->>EventRegistry: get_handlers(event.get_event_type())
    EventRegistry-->>MatcherManager: handlers_by_priority
    loop priorities
        MatcherManager->>MatcherManager: _simple_run(matcher_list,...)
        loop matcher in priority
            MatcherManager->>MatcherManager: prepare session_args, session_kwargs
            MatcherManager->>MatcherManager: collect runtime DependsFactory in args/kwargs
            alt has runtime dependencies
                MatcherManager->>DependsFactory: resolve(*session_args, **session_kwargs) concurrent
                DependsFactory-->>MatcherManager: resolved values or None
                alt any result is None
                    MatcherManager-->>MatcherManager: runtime resolve failed
                    MatcherManager-->>MatcherManager: raise RuntimeError
                end
            end
            MatcherManager->>MatcherManager: _resolve_dependencies(signature, session_args, session_kwargs)
            alt dependencies resolvable
                MatcherManager->>DependsFactory: resolve(... d_kw ...) concurrent
                DependsFactory-->>MatcherManager: injected kwargs or None
                alt any dependency None
                    MatcherManager-->>MatcherManager: skip this handler
                else all ok
                    MatcherManager->>Handler: await handler(*new_args, **f_kwargs)
                    alt handler raises PassException
                        MatcherManager-->>MatcherManager: continue to next handler
                    else handler raises CancelException or BlockException
                        MatcherManager-->>MatcherManager: stop processing, return False
                    else success
                        MatcherManager-->>MatcherManager: if matcher.block then stop
                    end
                end
            else not resolvable
                MatcherManager-->>MatcherManager: log warning and skip handler
            end
        end
    end
    MatcherManager-->>ChatObject: return
    deactivate MatcherManager
Loading

Sequence diagram for preset fallback retry flow

sequenceDiagram
    actor User
    participant ChatObject
    participant Adapter as ChatAdapter
    participant MatcherManager
    participant FallbackHandler

    User->>ChatObject: send_message()
    ChatObject->>ChatObject: build PreCompletionEvent and others
    ChatObject->>ChatObject: _process_chat()
    ChatObject->>ChatObject: get_context_messages()
    ChatObject->>ChatObject: send_messages = context.unwrap()

    loop term in 1..config.llm.max_fallbacks
        ChatObject->>ChatObject: used_preset.add(preset.name)
        ChatObject->>Adapter: call_completion(send_messages, config, preset)
        alt adapter succeeds
            Adapter-->>ChatObject: UniResponse chunks and text
            ChatObject->>ChatObject: response = final UniResponse
            ChatObject-->>User: stream chunks via yield_response
            %% break
        else adapter raises Exception e
            Adapter-->>ChatObject: Exception e
            ChatObject->>ChatObject: log warning with e and term
            ChatObject->>MatcherManager: trigger_event(FallbackContext(preset, e, config, context_wrap, term), config, exception_ignored=(FallbackFailed,))
            MatcherManager->>FallbackHandler: invoke on_preset_fallback handler
            alt handler updates event.preset
                FallbackHandler-->>MatcherManager: return
                MatcherManager-->>ChatObject: return
                ChatObject->>ChatObject: preset = event.preset
            else handler calls event.fail(reason)
                FallbackHandler-->>MatcherManager: raise FallbackFailed
                MatcherManager-->>ChatObject: propagate FallbackFailed
                ChatObject-->>User: error
                %% break
            end
        end
    end
    alt no successful attempt
        ChatObject-->>User: raise FallbackFailed("Max preset fallbacks retries exceeded.")
    end
Loading

Class diagram for matcher, dependency injection, and event registry

classDiagram
    class EventRegistry {
        +defaultdict~str, defaultdict~int, list~FunctionData~~ _event_handlers
        +register_handler(event_type: str, data: FunctionData)
        +get_handlers(event_type: str) defaultdict~int, list~FunctionData~~
        +get_all() defaultdict~str, defaultdict~int, list~FunctionData~~~~
        +_all() defaultdict~str, defaultdict~int, list~FunctionData~~~~
    }

    class FunctionData {
        +Callable[..., Awaitable~Any~~] function
        +inspect.Signature signature
        +FrameType frame
        +int priority
        +Matcher matcher
    }

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

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

    class MatcherFactory {
        +_resolve_dependencies(signature: inspect.Signature, session_args: Iterable~Any~, session_kwargs: dict~str, Any~) (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
        +_simple_run(matcher_list: list~FunctionData~, event: BaseEvent, config: AmritaConfig, exception_ignored: tuple~type~BaseException~~, ... , extra_args: tuple, extra_kwargs: dict~str, Any~) bool
        +trigger_event(event: BaseEvent, config: AmritaConfig, *args, exception_ignored: tuple~type~Exception~~, **kwargs) None
    }

    class ChatException {
    }

    class MatcherException {
    }

    class BlockException {
    }

    class CancelException {
    }

    class PassException {
    }

    MatcherManager <|-- MatcherFactory
    EventRegistry o--> FunctionData
    FunctionData --> Matcher
    Matcher ..> EventRegistry : registers
    DependsFactory ..> MatcherFactory : used by
    MatcherFactory ..> EventRegistry : reads
    ChatException <|-- MatcherException
    MatcherException <|-- BlockException
    MatcherException <|-- CancelException
    MatcherException <|-- PassException
    ChatException <.. MatcherException : type alias
Loading

Class diagram for events, fallback context, and config

classDiagram
    class EventTypeEnum {
        <<enumeration>>
        PRESET_FALLBACK
        COMPLETION
        MESSAGE
        MEMORY
        Nil
        BEFORE_COMPLETION
        validate(name: str) bool
    }

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

    class Event {
        +USER_INPUT user_input
        +ChatObject chat_object
        +get_event_type() EventTypeEnum|str
        +event_type EventTypeEnum|str
    }

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

    class FallbackFailed {
        +FallbackFailed(*value: object)
    }

    class AmritaConfig {
        +LLMConfig llm
    }

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

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

    class ModelConfig {
        +bool stream
        +bool support_tool
        +bool support_vision
        +bool cot_model
    }

    class SendMessageWrap {
    }

    class ChatObject {
        +ModelPreset preset
        +AmritaConfig config
        +SendMessageWrap context_wrap
    }

    BaseEvent <|-- Event
    BaseEvent <|-- FallbackContext
    EventTypeEnum <.. BaseEvent
    FallbackContext ..> ModelPreset
    FallbackContext ..> AmritaConfig
    FallbackContext ..> SendMessageWrap
    FallbackContext ..> FallbackFailed : fail()
    FallbackFailed --|> RuntimeError
    AmritaConfig --> LLMConfig
    ModelPreset --> ModelConfig
    ChatObject --> ModelPreset
    ChatObject --> AmritaConfig
    ChatObject --> SendMessageWrap
Loading

Class diagram for ModelAdapter, OpenAIAdapter, and AnthropicAdapter

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

    class OpenAIAdapter {
        +call_api(messages: Iterable~ChatCompletionMessageParam~, *args, **kwargs) AsyncGenerator~COMPLETION_RETURNING, None~
        +call_tools(tools: Iterable, *args, **kwargs) AsyncGenerator~Any, None~
        +get_adapter_protocol() str|tuple~str, ...~
    }

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

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

    class ModelConfig {
        +bool stream
        +bool support_tool
        +bool support_vision
        +bool cot_model
    }

    class AmritaConfig {
        +LLMConfig llm
    }

    class LLMConfig {
        +int max_tokens
        +int max_retries
        +int max_fallbacks
        +float temperature
        +float top_p
    }

    class UniResponseUsage~T_INT~ {
        +T_INT prompt_tokens
        +T_INT completion_tokens
        +T_INT total_tokens
    }

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

    ModelAdapter <|-- OpenAIAdapter
    ModelAdapter <|-- AnthropicAdapter
    ModelAdapter --> ModelPreset
    ModelAdapter --> AmritaConfig
    ModelPreset --> ModelConfig
    AmritaConfig --> LLMConfig
    OpenAIAdapter ..> UniResponse
    AnthropicAdapter ..> UniResponse
    UniResponse --> UniResponseUsage~int~
Loading

File-Level Changes

Change Details Files
Refactor matcher/event system to support dependency injection, priority-based handlers, and new preset fallback events.
  • Change EventRegistry to store handlers grouped by event type and priority, and expose get_handlers/get_all with deprecated _all alias.
  • Make Matcher store a Matcher reference in FunctionData, remove stored block flag from FunctionData, and add set_block plus deprecate cancel_matcher in favor of stop_process using CancelException.
  • Introduce DependsFactory/Depends for DI, including static _resolve_dependencies and async _do_runtime_resolve that resolve dependencies (including runtime hook_args/kwargs) concurrently with ExceptionGroup handling.
  • Replace MatcherManager with MatcherFactory that orchestrates dependency resolution and handler execution via _simple_run and an overloaded trigger_event API using event+config and optional extra args/kwargs and exception_ignored.
  • Extend EventTypeEnum and BaseEvent to support PRESET_FALLBACK, and add FallbackContext with fail() raising FallbackFailed for preset fallback handling.
src/amrita_core/hook/matcher.py
src/amrita_core/hook/event.py
src/amrita_core/hook/exception.py
src/amrita_core/hook/on.py
tests/test_matcher.py
Add preset fallback mechanism to ChatObject processing loop driven by FallbackContext events and configurable max_fallbacks.
  • Introduce FallbackFailed exception for terminal fallback failure and wire it into fallback handling.
  • Add max_fallbacks field to LLMConfig, and in ChatObject._process_chat loop, attempt call_completion with the current preset up to max_fallbacks times, tracking used presets and on failure emitting a FallbackContext via MatcherManager.trigger_event.
  • Allow fallback handlers to modify event.preset; if no change occurs, call event.fail, and if all fallbacks exhausted, raise FallbackFailed.
  • Update event docs (EN/ZH) and new FallbackContext API-reference docs to describe on_preset_fallback hook, properties, and usage patterns.
src/amrita_core/chatmanager.py
src/amrita_core/config.py
src/amrita_core/hook/event.py
src/amrita_core/hook/exception.py
src/amrita_core/hook/matcher.py
src/amrita_core/hook/on.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
Introduce Anthropic protocol adapter and adjust protocol/adapter interfaces for extensibility and streaming CoT support.
  • Add AnthropicAdapter implementing ModelAdapter.call_api for anthropic.messages API with streaming and non-streaming paths, normalizing usage into UniResponse and exposing anthropic/claude protocol identifiers.
  • Add helper model_dump to normalize message objects (BaseModel or dict) before sending to Anthropic.
  • Relax ModelAdapter.call_api protocol to accept *args/**kwargs, and update OpenAIAdapter.call_api signature accordingly while simplifying its UniResponse yield logic.
  • Update libchat._call_api wrapper to filter out ... segments when preset.config.cot_model is true, by tracking is_thinking while streaming adapter output.
  • Wire new anthropic and deprecated packages into project dependencies.
src/amrita_core/builtins/adapter.py
src/amrita_core/protocol.py
src/amrita_core/libchat.py
pyproject.toml
Tighten and simplify utility types and list utilities while dropping unused datetime helpers.
  • Adjust T_INT TypeVar bounds in types.py to include int
None explicitly.
  • Add cot_model boolean flag to ModelConfig for controlling CoT thinking-tag stripping behavior.
  • Remove get_current_datetime_timestamp and format_datetime_timestamp utilities and their tests, and make split_list generic and correctly typed to return list[list[T]].
  • Expand documentation and navigation for new DI and fallback concepts and APIs.
    • Add Depends/DependsFactory entries to API reference index and sidebars in both EN/ZH VitePress configs.
    • Create dedicated docs for Depends, DependsFactory, and FallbackContext (EN/ZH) explaining signatures, behavior, concurrency, and usage examples.
    • Enhance event concept docs (EN/ZH) with examples of mutating presets in precompletion hooks, on_preset_fallback hook usage, and a detailed section on the Depends-based dependency injection system, runtime DI via hook_args/kwargs, and best practices.
    docs/.vitepress/config.mts
    docs/guide/api-reference/index.md
    docs/zh/guide/api-reference/index.md
    docs/guide/concepts/event.md
    docs/zh/guide/concepts/event.md
    docs/guide/api-reference/classes/Depends.md
    docs/guide/api-reference/classes/DependsFactory.md
    docs/guide/api-reference/classes/FallbackContext.md
    docs/zh/guide/api-reference/classes/Depends.md
    docs/zh/guide/api-reference/classes/DependsFactory.md
    docs/zh/guide/api-reference/classes/FallbackContext.md

    Assessment against linked issues

    Issue Objective Addressed Explanation
    #22 Introduce a model-fallback hook decorator (on_model_fallback or an equivalent) that is registered in the existing event system and fired when model calls fail.
    #22 Provide a dedicated fallback context object (e.g., FallbackContext) containing the failed preset, error information, configuration, and original message context, with type-safe definitions and documentation.
    #22 Allow fallback handlers to implement custom fallback behavior (e.g., cached responses or alternative model calls) using the existing async-generator response pattern (yielding responses) integrated into the model calling pipeline. Fallback handlers are implemented as async functions that modify the preset via FallbackContext and rely on ChatObject to retry the model call; they do not follow the requested async-generator pattern that yields fallback responses directly, nor do they provide a standardized way for handlers themselves to stream or emit responses.

    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
    Copy link
    Copy Markdown
    Contributor

    sourcery-ai Bot commented Feb 26, 2026

    Sorry @JohnRichard4096, you have reached your weekly rate limit of 500000 diff characters.

    Please try again later or upgrade to continue using Sourcery

    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 4 issues, and left some high level feedback:

    • In model_dump, the list comprehension reuses the parameter name (for obj in obj), which both shadows the outer variable and is confusing; use a different loop variable (e.g. for item in obj) to avoid bugs and improve readability.
    • The T_INT TypeVar in types.py is declared as TypeVar("T_INT", int, None, int | None), which mixes duplicate and incompatible constraints; this will fail at import time and should be simplified to valid constraints (e.g. TypeVar("T_INT", int, None) or a proper bound).
    • The exception_ignored argument for MatcherFactory.trigger_event is now a keyword-only parameter, but ChatObject._process_chat calls MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,)) positionally, so the exceptions won't actually be treated as ignored; update the call site to exception_ignored=(FallbackFailed,).
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - In `model_dump`, the list comprehension reuses the parameter name (`for obj in obj`), which both shadows the outer variable and is confusing; use a different loop variable (e.g. `for item in obj`) to avoid bugs and improve readability.
    - The `T_INT` TypeVar in `types.py` is declared as `TypeVar("T_INT", int, None, int | None)`, which mixes duplicate and incompatible constraints; this will fail at import time and should be simplified to valid constraints (e.g. `TypeVar("T_INT", int, None)` or a proper bound).
    - The `exception_ignored` argument for `MatcherFactory.trigger_event` is now a keyword-only parameter, but `ChatObject._process_chat` calls `MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,))` positionally, so the exceptions won't actually be treated as ignored; update the call site to `exception_ignored=(FallbackFailed,)`.
    
    ## 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` in `isinstance` is unsafe and will raise a `TypeError`.
    
    Here, using a PEP 604 union (`CancelException | BlockException`) as the second argument to `isinstance` will always raise `TypeError` at runtime, breaking the exception handling path. Please change this to `isinstance(e, (CancelException, BlockException))` so the check works correctly.
    </issue_to_address>
    
    ### Comment 2
    <location path="src/amrita_core/chatmanager.py" line_range="840-841" />
    <code_context>
    +                logger.warning(
    +                    f"Because of `{e!s}`, LLM request failed, retrying ({i}/{self.config.llm.max_retries})..."
    +                )
    +                ctx = FallbackContext(self.preset, e, self.config, self.context_wrap, i)
    +                await MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,))
    +                if ctx.preset is self.preset:
    +                    ctx.fail("No preset fallback available, exiting!")
    </code_context>
    <issue_to_address>
    **issue (bug_risk):** `FallbackFailed` is passed positionally and never reaches `exception_ignored` in `trigger_event`.
    
    Here `(FallbackFailed,)` goes into `*args`, so `exception_ignored` stays as its default `()`. That means `FallbackFailed` is not actually treated as an ignored/propagated exception. If you intend it to be ignored, call this as `await MatcherManager.trigger_event(ctx, ctx.config, exception_ignored=(FallbackFailed,))` (or via `**kwargs`).
    </issue_to_address>
    
    ### Comment 3
    <location path="src/amrita_core/chatmanager.py" line_range="837-839" />
    <code_context>
    +                        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})..."
    +                )
    +                ctx = FallbackContext(self.preset, e, self.config, self.context_wrap, i)
    </code_context>
    <issue_to_address>
    **suggestion:** Retry log message uses `max_retries` while control flow is governed by `max_fallbacks`.
    
    Within the preset fallback loop (`for i in range(1, self.config.llm.max_fallbacks + 1)`), the warning logs `({i}/{self.config.llm.max_retries})`, which is misleading because the loop is actually bounded by `max_fallbacks`. Please either use `max_fallbacks` in the log or reword it to clearly distinguish network retries from preset fallbacks.
    
    ```suggestion
                    logger.warning(
                        f"Because of `{e!s}`, LLM request failed, retrying preset fallback ({i}/{self.config.llm.max_fallbacks})..."
                    )
    ```
    </issue_to_address>
    
    ### Comment 4
    <location path="tests/test_utils.py" line_range="22-24" />
    <code_context>
    -    assert "00:00:00" in result or "AM" in result
    -
    -
     def test_split_list():
         lst = [1, 2, 3, 4, 5, 6]
         assert split_list(lst, 2) == [[1, 2], [3, 4], [5, 6]]
    </code_context>
    <issue_to_address>
    **suggestion (testing):** Add edge-case coverage for `split_list` (threshold 1, equal to length, and greater than length).
    
    The current `test_split_list` only covers the evenly divisible case. To better exercise the new generic implementation, please also test:
    
    - `threshold == len(lst)` → returns `[lst]`
    - `threshold > len(lst)` → returns `[lst]`
    - `threshold == 1` → returns a list of single-element lists
    
    These cases help detect off-by-one issues and future regressions in the split logic.
    </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 in isinstance is unsafe and will raise a TypeError.

    Here, using a PEP 604 union (CancelException | BlockException) as the second argument to isinstance will always raise TypeError at runtime, breaking the exception handling path. Please change this to isinstance(e, (CancelException, BlockException)) so the check works correctly.

    Comment on lines +840 to +841
    ctx = FallbackContext(self.preset, e, self.config, self.context_wrap, i)
    await MatcherManager.trigger_event(ctx, ctx.config, (FallbackFailed,))
    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): FallbackFailed is passed positionally and never reaches exception_ignored in trigger_event.

    Here (FallbackFailed,) goes into *args, so exception_ignored stays as its default (). That means FallbackFailed is not actually treated as an ignored/propagated exception. If you intend it to be ignored, call this as await MatcherManager.trigger_event(ctx, ctx.config, exception_ignored=(FallbackFailed,)) (or via **kwargs).

    Comment on lines +837 to +839
    logger.warning(
    f"Because of `{e!s}`, LLM request failed, retrying ({i}/{self.config.llm.max_retries})..."
    )
    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.

    suggestion: Retry log message uses max_retries while control flow is governed by max_fallbacks.

    Within the preset fallback loop (for i in range(1, self.config.llm.max_fallbacks + 1)), the warning logs ({i}/{self.config.llm.max_retries}), which is misleading because the loop is actually bounded by max_fallbacks. Please either use max_fallbacks in the log or reword it to clearly distinguish network retries from preset fallbacks.

    Suggested change
    logger.warning(
    f"Because of `{e!s}`, LLM request failed, retrying ({i}/{self.config.llm.max_retries})..."
    )
    logger.warning(
    f"Because of `{e!s}`, LLM request failed, retrying preset fallback ({i}/{self.config.llm.max_fallbacks})..."
    )

    Comment thread tests/test_utils.py
    Comment on lines 22 to 24
    def test_split_list():
    lst = [1, 2, 3, 4, 5, 6]
    assert split_list(lst, 2) == [[1, 2], [3, 4], [5, 6]]
    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.

    suggestion (testing): Add edge-case coverage for split_list (threshold 1, equal to length, and greater than length).

    The current test_split_list only covers the evenly divisible case. To better exercise the new generic implementation, please also test:

    • threshold == len(lst) → returns [lst]
    • threshold > len(lst) → returns [lst]
    • threshold == 1 → returns a list of single-element lists

    These cases help detect off-by-one issues and future regressions in the split logic.

    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