Skip to content

Introduce multimodal MessageContent and session-aware chat hooks#7

Merged
JohnRichard4096 merged 2 commits intomainfrom
feat/upd
Feb 12, 2026
Merged

Introduce multimodal MessageContent and session-aware chat hooks#7
JohnRichard4096 merged 2 commits intomainfrom
feat/upd

Conversation

@JohnRichard4096
Copy link
Member

@JohnRichard4096 JohnRichard4096 commented Feb 12, 2026

Summary by Sourcery

Introduce a unified multimodal message content abstraction and strengthen session-awareness across the chat pipeline while updating docs and dependencies accordingly.

New Features:

  • Add extensible MessageContent hierarchy supporting strings, raw data, metadata-enriched messages, and images, including URL fetching, base64 embedding, and file saving.
  • Allow model adapters and completion utilities to return rich message content types instead of plain strings, enabling multimodal responses.
  • Expose session data and customizable hook arguments from ChatObject to event matchers for more context-aware processing.

Enhancements:

  • Refine USER_INPUT typing to support generic content sequences and optional input for greater flexibility.
  • Rename MCP tool property casting helpers from OpenAI-specific to Amrita-specific naming to better reflect their purpose.
  • Improve architecture documentation (EN/ZH) with detailed diagrams and explanations of core, session management, and agent loop isolation mechanisms.

Build:

  • Bump project version to 0.4.1 and add aiofiles, aiohttp, and filetype as runtime dependencies to support new image and I/O capabilities.

Documentation:

  • Extend English and Chinese architecture guides with sections describing core architecture, global vs. session containers, and session-isolated agent loops.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 12, 2026

Reviewer's Guide

Introduces a more flexible multimodal message content system (including images and metadata), extends session-aware chat handling and matcher hooks, refines MCP tool property casting naming, updates documentation to describe sessions and architecture, and updates dependencies and versioning to support new async I/O and image handling.

Sequence diagram for session-aware matcher hook invocation in ChatObject._process_chat

sequenceDiagram
    participant User
    participant SessionsManager
    participant ChatObject
    participant MatcherManager
    participant Hook as MatcherHook

    User->>ChatObject: construct(user_input, session_id, auto_create_session, hook_args, hook_kwargs)
    ChatObject->>SessionsManager: is_session_registered(session_id)
    alt session not registered and auto_create_session
        ChatObject->>SessionsManager: init_session(session_id)
    end
    ChatObject->>SessionsManager: get_session_data(session_id, None)
    SessionsManager-->>ChatObject: SessionData or None
    ChatObject->>ChatObject: store session and hook_args, hook_kwargs

    User->>ChatObject: start _process_chat()
    ChatObject->>MatcherManager: trigger_event(ChatEvent, config, ChatObject, preset, *hook_args, session=session, **hook_kwargs)
    MatcherManager->>Hook: handle input event(ChatEvent, ChatObject, preset, session, *hook_args, **hook_kwargs)
    Hook-->>MatcherManager: possibly modified ChatEvent
    MatcherManager-->>ChatObject: return

    ChatObject->>ChatObject: update messages from ChatEvent

    ChatObject-->>User: stream model response

    ChatObject->>MatcherManager: trigger_event(CompletionEvent, config, ChatObject, preset, *hook_args, session=session, **hook_kwargs)
    MatcherManager->>Hook: handle completion event(CompletionEvent, ChatObject, preset, session, *hook_args, **hook_kwargs)
    Hook-->>MatcherManager: possibly modified CompletionEvent
    MatcherManager-->>ChatObject: return

    ChatObject-->>User: final response
Loading

Class diagram for the new multimodal MessageContent hierarchy

classDiagram
    class MessageContent {
        +str type
        +MessageContent(content_type str)
        +str __str__()
        +get_content()
    }

    class RawMessageContent {
        +Any raw_data
        +RawMessageContent(raw_data Any)
        +get_content() Any
    }

    class StringMessageContent {
        +str text
        +StringMessageContent(text str)
        +get_content() str
    }

    class MessageMetadata {
        +str content
        +dict~str, Any~ metadata
    }

    class MessageWithMetadata {
        +str content
        +dict~str, Any~ metadata
        +MessageWithMetadata(content str, metadata dict~str, Any~)
        +get_content() Any
        +get_metadata() dict
        +get_full_content() MessageMetadata
    }

    class ImageMessage {
        +str|BytesIO|bytes image
        +ImageMessage(image str|BytesIO|bytes)
        +async get_image(headers dict~str, None~) BytesIO|bytes
        +async curl_image(extra_headers dict) bytes
        +get_content() str
        +async save_to(path Path, headers dict)
    }

    class COMPLETION_RETURNING {
        <<typealias>>
        +MessageContent | str | UniResponse~str, None~
    }

    MessageContent <|-- RawMessageContent
    MessageContent <|-- StringMessageContent
    MessageContent <|-- MessageWithMetadata
    MessageContent <|-- ImageMessage
Loading

Class diagram for ChatObject session and matcher hook integration

classDiagram
    class ChatObject {
        +bool train
        +str session_id
        +Any data
        +ModelPreset preset
        +AmritaConfig config
        +SessionData session
        +asyncio.Queue _response_queue
        +asyncio.Queue _overflow_queue
        +bool _is_running
        +bool _queue_done
        +bool _has_consumer
        +tuple~Any~ _hook_args
        +dict~str, Any~ _hook_kwargs
        +ChatObject(user_input Any, session_id str, train bool, context Any, config AmritaConfig, preset ModelPreset, auto_create_session bool, hook_args tuple~Any~, hook_kwargs dict~str, Any~, queue_size int, overflow_queue_size int)
        +async _process_chat()
    }

    class SessionsManager {
        +is_session_registered(session_id str) bool
        +init_session(session_id str)
        +get_session_data(session_id str, default SessionData) SessionData
    }

    class SessionData {
        +Any memory
        +ModelPreset preset
    }

    class MatcherManager {
        +async trigger_event(event Any, config AmritaConfig, chat ChatObject, preset ModelPreset, *hook_args Any, session SessionData, **hook_kwargs Any)
    }

    ChatObject --> SessionsManager : obtains SessionData
    ChatObject --> SessionData : holds session
    ChatObject --> MatcherManager : trigger_event(..., session, hook_args, hook_kwargs)
Loading

File-Level Changes

Change Details Files
Refactor message content abstraction into protocol layer and add multimodal/image support, adjusting completion return typing.
  • Introduce MessageContent hierarchy in protocol module, including raw, string, metadata-bearing, and image message classes with async image fetching and base64 markdown rendering.
  • Add get_image_format helper using filetype to infer image MIME/extension and define COMPLETION_RETURNING union type for adapters.
  • Update ModelAdapter.call_api, OpenAIAdapter.call_api, and libchat.call_completion/get_last_response to work with the new COMPLETION_RETURNING types instead of plain strings.
src/amrita_core/protocol.py
src/amrita_core/builtins/adapter.py
src/amrita_core/libchat.py
Unify and relax user input typing for multimodal content in core types.
  • Change USER_INPUT to accept generic Content sequences, strings, or None, and simplify the associated generic type variable bound.
  • Document adherence to OpenAI-style naming while allowing broader content types.
src/amrita_core/types.py
Rename MCP tool property casting helpers from OpenAI-specific to Amrita-specific naming while keeping behavior.
  • Rename cast_mcp_properties_to_openai to cast_mcp_properties_to_amrita in tools models and MCP integration.
  • Update MCPTool helper methods so tools are formatted for OpenAI while using the new Amrita-centric naming and casting function.
src/amrita_core/tools/models.py
src/amrita_core/tools/mcp.py
Make ChatObject session-aware and allow passing hook arguments into MatcherManager events.
  • Inject SessionData into ChatObject at construction, storing session and using session memory as context when available.
  • Extend ChatObject constructor to accept hook_args and hook_kwargs for matcher hooks and store them on the instance.
  • Update MatcherManager.trigger_event invocations for ChatEvent and CompletionEvent to pass ChatObject, preset, session, and any hook args/kwargs so matchers have richer context.
src/amrita_core/chatmanager.py
Expand architecture documentation to cover sessions, global containers, and agent loop/session isolation in both English and Chinese guides.
  • Add diagrams and narrative explaining global container vs sessions, session conversation context, and parallel agent loop handling in the English architecture guide.
  • Mirror the new session and isolation architecture documentation and diagrams in the Chinese guide, including explanation of global resource sharing and context isolation.
docs/guide/getting-started/architecture.md
docs/zh/guide/getting-started/architecture.md
Update project metadata and dependencies to support new async file and image handling.
  • Bump project version to 0.4.1 to reflect new features.
  • Add aiofiles, aiohttp, and filetype as required dependencies for async image download and format detection.
pyproject.toml
uv.lock

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

Copy link
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 ChatObject.__init__, hook_kwargs uses a mutable default {} which can lead to state being shared across instances; consider defaulting to None and initializing a new dict inside the method.
  • The ImageMessage.get_image and curl_image signatures use dict[str, None] | None and untyped dict for headers, but the actual usage expects standard HTTP header values (typically str); tightening these type hints (e.g., dict[str, str] | None) will make the API clearer and avoid type confusion.
  • Now that call_api and call_completion can yield MessageContent instead of raw strings, double-check any downstream consumers (e.g., places using RESPONSE_TYPE or assuming str chunks) to ensure they correctly handle the new MessageContent types rather than relying on implicit str behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ChatObject.__init__`, `hook_kwargs` uses a mutable default `{}` which can lead to state being shared across instances; consider defaulting to `None` and initializing a new dict inside the method.
- The `ImageMessage.get_image` and `curl_image` signatures use `dict[str, None] | None` and untyped `dict` for headers, but the actual usage expects standard HTTP header values (typically `str`); tightening these type hints (e.g., `dict[str, str] | None`) will make the API clearer and avoid type confusion.
- Now that `call_api` and `call_completion` can yield `MessageContent` instead of raw strings, double-check any downstream consumers (e.g., places using `RESPONSE_TYPE` or assuming `str` chunks) to ensure they correctly handle the new `MessageContent` types rather than relying on implicit `str` behavior.

## Individual Comments

### Comment 1
<location> `src/amrita_core/protocol.py:109-110` </location>
<code_context>
+        super().__init__("image")
+        self.image: str | BytesIO | bytes = image
+
+    async def get_image(
+        self, headers: dict[str, None] | None = None
+    ) -> BytesIO | bytes:
+        if isinstance(self.image, str):
</code_context>

<issue_to_address>
**issue:** The headers type annotation is overly restrictive and inconsistent with `curl_image`.

`headers` is typed as `dict[str, None] | None`, which implies all values are `None`, but `curl_image` does `headers.update(extra_headers or {})` and HTTP headers normally have `str` values. `curl_image` also uses `dict | None` for `extra_headers`, so the two signatures diverge. Please align them on a more accurate type like `Mapping[str, str] | None` (or `dict[str, str] | None`) for both methods.
</issue_to_address>

### Comment 2
<location> `src/amrita_core/protocol.py:70-79` </location>
<code_context>
+                    return obj
+        raise ValueError("Image must be a URL to use this method")
+
+    def get_content(self) -> str:
+        if isinstance(self.image, str):
+            return f"![]({self.image})"
+        elif isinstance(self.image, BytesIO):
+            self.image = self.image.getvalue()
+        image_type = get_image_format(self.image)
+        if not image_type:
+            return "[Unsupported image format]"
+        return f"![](data:image/{image_type};base64,{base64.b64encode(self.image).decode()})"
+
+    async def save_to(self, path: Path, headers: dict | None = None):
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Clarify whether `bytes` represent raw image bytes or already-base64-encoded data to avoid double-encoding.

The docstring says `bytes` are “Base64 encoded image”, but `get_content` treats `self.image` as raw bytes and calls `base64.b64encode(self.image)`, which would double-encode already-base64 input and break the data URL. Please either update the docstring to say the `bytes` are raw image bytes, or adjust `get_content` to skip `b64encode` when `self.image` is documented/expected to be base64-encoded already.
</issue_to_address>

### Comment 3
<location> `src/amrita_core/chatmanager.py:403-404` </location>
<code_context>
         config: AmritaConfig | None = None,
         preset: ModelPreset | None = None,
         auto_create_session: bool = False,
+        hook_args: tuple[Any, ...] = (),
+        hook_kwargs: dict[str, Any] = {},
         queue_size: int = 25,
         overflow_queue_size: int = 45,
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid using a mutable dict as a default argument for `hook_kwargs`.

Using `{}` as the default means all callers that rely on the default share the same dict, which can cause unintended state leakage between instances. Use `hook_kwargs: dict[str, Any] | None = None` and in `__init__` set `self._hook_kwargs = hook_kwargs or {}` instead.
</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.

@cloudflare-workers-and-pages
Copy link

Deploying amritacore with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6dbd97c
Status: ✅  Deploy successful!
Preview URL: https://9068a8f7.amritacore.pages.dev
Branch Preview URL: https://feat-upd.amritacore.pages.dev

View logs

@JohnRichard4096
Copy link
Member Author

@sourcery-ai title

@sourcery-ai sourcery-ai bot changed the title feat: multimodal and types Introduce multimodal MessageContent and session-aware chat hooks Feb 12, 2026
@JohnRichard4096 JohnRichard4096 merged commit c962dcf into main Feb 12, 2026
2 checks passed
@JohnRichard4096 JohnRichard4096 deleted the feat/upd branch February 12, 2026 06:35
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.

1 participant