Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions modules/chat/protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Target Protocol interfaces for the Chat domain.

These Protocols describe the *ideal* service contracts. Currently
session CRUD lives in ``service.py`` while LLM generation and
streaming live in the orchestrator; the Protocols define the target
unified facade.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable


if TYPE_CHECKING:
from collections.abc import AsyncIterator

from modules.chat.schemas import (
MessageInfo,
SessionInfo,
SessionSummary,
ShareInfo,
StreamChunk,
)
from modules.llm.schemas import LLMConfig


@runtime_checkable
class ChatService(Protocol):
"""Unified facade for chat sessions, messages, and generation.

Combines session/message CRUD (currently ``modules/chat/service.py``)
with LLM generation (currently orchestrator) into a single contract.
"""

# -- Sessions -------------------------------------------------------------

async def create_session(
self,
*,
source: str = "admin",
source_id: str | None = None,
title: str | None = None,
system_prompt: str | None = None,
owner_id: int | None = None,
workspace_id: int = 1,
) -> SessionInfo:
"""Create a new chat session."""
...

async def get_session(self, session_id: str) -> SessionInfo | None:
"""Look up a session by ID."""
...

async def list_sessions(
self,
*,
owner_id: int | None = None,
workspace_id: int | None = None,
) -> list[SessionSummary]:
"""List sessions as compact summaries."""
...

async def delete_session(self, session_id: str) -> bool:
"""Delete a session and all its messages."""
...

# -- Messages (CRUD) ------------------------------------------------------

async def get_history(self, session_id: str) -> list[MessageInfo]:
"""Return the active message branch for a session."""
...

async def add_message(
self,
session_id: str,
role: str,
content: str,
*,
parent_id: str | None = None,
) -> MessageInfo:
"""Append a message to the session."""
...

# -- Generation (LLM) ----------------------------------------------------

async def send_message(
self,
session_id: str,
content: str,
*,
llm_config: LLMConfig | None = None,
) -> MessageInfo:
"""Send a user message and return the assistant reply."""
...

async def stream_message(
self,
session_id: str,
content: str,
*,
llm_config: LLMConfig | None = None,
) -> AsyncIterator[StreamChunk]:
"""Send a user message and stream the assistant reply."""
...

# -- Sharing --------------------------------------------------------------

async def share_session(
self,
session_id: str,
user_id: int,
*,
permission: str = "read",
) -> ShareInfo:
"""Grant another user access to a session."""
...

async def unshare_session(
self,
session_id: str,
user_id: int,
) -> bool:
"""Revoke a user's access to a session."""
...
99 changes: 99 additions & 0 deletions modules/chat/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Ideal data shapes for the Chat domain.

These TypedDicts describe the *target* API contract for chat sessions,
messages, and sharing.
"""

from __future__ import annotations

from typing import TypedDict


# ---------------------------------------------------------------------------
# Sessions
# ---------------------------------------------------------------------------


class SessionInfo(TypedDict):
"""Read-only view of a chat session."""

id: str
title: str
system_prompt: str | None
pinned: bool
source: str | None # "admin" | "telegram" | "widget" | "whatsapp" | "mobile"
source_id: str | None
owner_id: int | None
rag_mode: str | None # "all" | "selected" | "collection" | "none"
collection_ids: list[int] | None
created: str | None
updated: str | None


class SessionSummary(TypedDict):
"""Compact session view for listing (no messages, no system_prompt)."""

id: str
title: str
pinned: bool
message_count: int
last_message: str | None # first 100 chars
source: str | None
owner_id: int | None
created: str | None
updated: str | None


# ---------------------------------------------------------------------------
# Messages
# ---------------------------------------------------------------------------


class MessageInfo(TypedDict):
"""Read-only view of a chat message."""

id: str
role: str # "user" | "assistant" | "system"
content: str
edited: bool
timestamp: str | None
parent_id: str | None
is_active: bool
metadata: dict | None


class StreamChunk(TypedDict, total=False):
"""A single chunk from ChatService.stream_message().

``content`` — text delta from the model.
``done`` — ``True`` on the final chunk (includes ``token_usage``).
"""

content: str
done: bool
token_usage: TokenUsage | None


class TokenUsage(TypedDict):
"""Token budget information."""

tokens: int
context_window: int
percent: float
trimmed: bool


# ---------------------------------------------------------------------------
# Sharing
# ---------------------------------------------------------------------------


class ShareInfo(TypedDict):
"""Read-only view of a session share entry."""

id: int
session_id: str
user_id: int
permission: str # "read" | "write"
shared_by: int | None
shared_at: str | None
102 changes: 102 additions & 0 deletions modules/core/protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Target Protocol interfaces for the Core domain (auth & users).

These Protocols describe the *ideal* service contracts. Currently
auth logic lives in ``auth_manager.py`` (module-level functions);
the Protocols define the target class-based facade.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable


if TYPE_CHECKING:
from modules.core.schemas import (
LoginResult,
RoleInfo,
UserInfo,
WorkspaceInfo,
WorkspaceMemberInfo,
)


@runtime_checkable
class AuthService(Protocol):
"""Authentication, token management, and RBAC.

Currently scattered across ``auth_manager.py`` (functions) and
``modules/core/service.py`` (UserService, RoleService,
WorkspaceService). This Protocol defines the unified target.
"""

# -- Authentication -------------------------------------------------------

async def authenticate(
self,
username: str,
password: str,
) -> LoginResult:
"""Validate credentials and return an access token + user info."""
...

async def validate_token(self, token: str) -> UserInfo | None:
"""Decode and validate a JWT. Returns ``None`` if invalid/expired."""
...

async def revoke_session(self, jti: str) -> bool:
"""Revoke a single session by its JWT ID."""
...

async def revoke_all_sessions(self, user_id: int) -> int:
"""Revoke all active sessions for a user. Returns count revoked."""
...

# -- Permissions ----------------------------------------------------------

async def get_permissions(self, user_id: int) -> dict[str, str]:
"""Return the effective permission map for a user.

Keys are module names, values are access levels
(``"view"`` | ``"edit"`` | ``"manage"``).
"""
...

async def has_permission(
self,
user_id: int,
module: str,
min_level: str = "view",
) -> bool:
"""Check whether a user meets the minimum access level for a module."""
...

# -- User management ------------------------------------------------------

async def get_user(self, user_id: int) -> UserInfo | None:
"""Look up a user by ID."""
...

async def list_users(
self,
*,
workspace_id: int | None = None,
include_inactive: bool = False,
) -> list[UserInfo]:
"""List users, optionally filtered by workspace."""
...

# -- Roles ----------------------------------------------------------------

async def get_roles(self) -> list[RoleInfo]:
"""List all RBAC roles with their permission maps."""
...

# -- Workspaces -----------------------------------------------------------

async def get_workspace(self, workspace_id: int) -> WorkspaceInfo | None:
"""Look up a workspace by ID."""
...

async def list_members(self, workspace_id: int) -> list[WorkspaceMemberInfo]:
"""List members of a workspace."""
...
Loading
Loading