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
24 changes: 22 additions & 2 deletions src/askui/chat/api/assistants/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ def retrieve(
) -> Assistant:
try:
assistant_path = self._get_assistant_path(assistant_id)
assistant = Assistant.model_validate_json(assistant_path.read_text())
content = assistant_path.read_text()
if not content.strip():
error_msg = f"Assistant {assistant_id} not found"
raise NotFoundError(error_msg)
assistant = Assistant.model_validate_json(content)
if not (
assistant.workspace_id is None or assistant.workspace_id == workspace_id
):
Expand All @@ -59,6 +63,10 @@ def retrieve(
except FileNotFoundError as e:
error_msg = f"Assistant {assistant_id} not found"
raise NotFoundError(error_msg) from e
except (ValueError, TypeError) as e:
# Handle JSON parsing errors
error_msg = f"Assistant {assistant_id} not found"
raise NotFoundError(error_msg) from e
else:
return assistant

Expand All @@ -76,6 +84,9 @@ def modify(
params: AssistantModifyParams,
) -> Assistant:
assistant = self.retrieve(workspace_id, assistant_id)
if assistant.workspace_id is None:
error_msg = f"Default assistant {assistant_id} cannot be modified"
raise ForbiddenError(error_msg)
modified = assistant.modify(params)
self._save(modified)
return modified
Expand All @@ -91,10 +102,19 @@ def delete(
if assistant.workspace_id is None and not force:
error_msg = f"Default assistant {assistant_id} cannot be deleted"
raise ForbiddenError(error_msg)
self._get_assistant_path(assistant_id).unlink()
try:
self._get_assistant_path(assistant_id).unlink()
except FileNotFoundError:
# File already deleted, that's fine
pass
except FileNotFoundError as e:
error_msg = f"Assistant {assistant_id} not found"
raise NotFoundError(error_msg) from e
except NotFoundError:
# If force=True and assistant doesn't exist, just ignore
if not force:
raise
# For force=True, we can ignore the NotFoundError

def _save(self, assistant: Assistant, new: bool = False) -> None:
self._assistants_dir.mkdir(parents=True, exist_ok=True)
Expand Down
9 changes: 8 additions & 1 deletion src/askui/chat/api/mcp_configs/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ def modify(
params: McpConfigModifyParams,
) -> McpConfig:
mcp_config = self.retrieve(workspace_id, mcp_config_id)
if mcp_config.workspace_id is None:
error_msg = f"Default MCP configuration {mcp_config_id} cannot be modified"
raise ForbiddenError(error_msg)
modified = mcp_config.modify(params)
self._save(modified)
return modified
Expand All @@ -129,7 +132,11 @@ def delete(
self._get_mcp_config_path(mcp_config_id).unlink()
except FileNotFoundError as e:
error_msg = f"MCP configuration {mcp_config_id} not found"
raise NotFoundError(error_msg) from e
if not force:
raise NotFoundError(error_msg) from e
except NotFoundError:
if not force:
raise

def _save(self, mcp_config: McpConfig, new: bool = False) -> None:
self._mcp_configs_dir.mkdir(parents=True, exist_ok=True)
Expand Down
91 changes: 91 additions & 0 deletions src/askui/chat/api/messages/chat_history_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from anthropic.types.beta import BetaTextBlockParam, BetaToolUnionParam

from askui.chat.api.messages.models import Message, MessageCreateParams
from askui.chat.api.messages.service import MessageService
from askui.chat.api.messages.translator import MessageTranslator
from askui.chat.api.models import ThreadId
from askui.models.shared.agent_message_param import MessageParam
from askui.models.shared.truncation_strategies import TruncationStrategyFactory


class ChatHistoryManager:
"""
Manages chat history by providing methods to retrieve and add messages.

This service encapsulates the interaction between MessageService and MessageTranslator
to provide a clean interface for managing chat history in the context of AI agents.
"""

def __init__(
self,
message_service: MessageService,
message_translator: MessageTranslator,
truncation_strategy_factory: TruncationStrategyFactory,
) -> None:
"""
Initialize the chat history manager.

Args:
message_service (MessageService): Service for managing message persistence.
message_translator (MessageTranslator): Translator for converting between
message formats.
truncation_strategy_factory (TruncationStrategyFactory): Factory for creating truncation strategies.
"""
self._message_service = message_service
self._message_translator = message_translator
self._message_content_translator = message_translator.content_translator
self._truncation_strategy_factory = truncation_strategy_factory

async def retrieve_message_params(
self,
thread_id: ThreadId,
model: str,
system: str | list[BetaTextBlockParam] | None,
tools: list[BetaToolUnionParam],
) -> list[MessageParam]:
truncation_strategy = (
self._truncation_strategy_factory.create_truncation_strategy(
system=system,
tools=tools,
messages=[],
model=model,
)
)
for msg in self._message_service.iter(thread_id=thread_id):
anthropic_message = await self._message_translator.to_anthropic(msg)
truncation_strategy.append_message(anthropic_message)
return truncation_strategy.messages

async def append_message(
self,
thread_id: ThreadId,
assistant_id: str | None,
run_id: str,
message: MessageParam,
) -> Message:
"""
Add a message to the chat history and return both the created message and original message param.

This method creates a message in the database and returns both the created
message object and the original message parameter for further processing.

Args:
thread_id (ThreadId): The thread ID to add the message to.
assistant_id (str | None): The assistant ID if the message is from an assistant.
run_id (str): The run ID associated with this message.
message (MessageParam): The message to add.

Returns:
Message: The created message object
"""
return self._message_service.create(
thread_id=thread_id,
params=MessageCreateParams(
assistant_id=assistant_id if message.role == "assistant" else None,
role=message.role,
content=await self._message_content_translator.from_anthropic(
message.content
),
run_id=run_id,
),
)
27 changes: 27 additions & 0 deletions src/askui/chat/api/messages/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from askui.chat.api.dependencies import WorkspaceDirDep
from askui.chat.api.files.dependencies import FileServiceDep
from askui.chat.api.files.service import FileService
from askui.chat.api.messages.chat_history_manager import ChatHistoryManager
from askui.chat.api.messages.service import MessageService
from askui.chat.api.messages.translator import MessageTranslator
from askui.models.shared.truncation_strategies import (
SimpleTruncationStrategyFactory,
TruncationStrategyFactory,
)


def get_message_service(
Expand All @@ -26,3 +31,25 @@ def get_message_translator(


MessageTranslatorDep = Depends(get_message_translator)


def get_truncation_strategy_factory() -> TruncationStrategyFactory:
return SimpleTruncationStrategyFactory()


TruncationStrategyFactoryDep = Depends(get_truncation_strategy_factory)


def get_chat_history_manager(
message_service: MessageService = MessageServiceDep,
message_translator: MessageTranslator = MessageTranslatorDep,
truncation_strategy_factory: TruncationStrategyFactory = TruncationStrategyFactoryDep,
) -> ChatHistoryManager:
return ChatHistoryManager(
message_service=message_service,
message_translator=message_translator,
truncation_strategy_factory=truncation_strategy_factory,
)


ChatHistoryManagerDep = Depends(get_chat_history_manager)
21 changes: 21 additions & 0 deletions src/askui/chat/api/messages/service.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from pathlib import Path
from typing import Iterator

from askui.chat.api.messages.models import Message, MessageCreateParams
from askui.chat.api.models import MessageId, ThreadId
from askui.utils.api_utils import (
LIST_LIMIT_DEFAULT,
ConflictError,
ListOrder,
ListQuery,
ListResponse,
NotFoundError,
Expand Down Expand Up @@ -40,6 +43,24 @@ def list_(self, thread_id: ThreadId, query: ListQuery) -> ListResponse[Message]:
messages_dir = self.get_messages_dir(thread_id)
return list_resources(messages_dir, query, Message)

def iter(
self,
thread_id: ThreadId,
order: ListOrder = "asc",
batch_size: int = LIST_LIMIT_DEFAULT,
) -> Iterator[Message]:
has_more = True
last_id: str | None = None
while has_more:
list_messages_response = self.list_(
thread_id=thread_id,
query=ListQuery(limit=batch_size, order=order, after=last_id),
)
has_more = list_messages_response.has_more
last_id = list_messages_response.last_id
for msg in list_messages_response.data:
yield msg

def retrieve(self, thread_id: ThreadId, message_id: MessageId) -> Message:
try:
message_file = self._get_message_path(thread_id, message_id)
Expand Down
12 changes: 4 additions & 8 deletions src/askui/chat/api/runs/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,23 @@
from askui.chat.api.dependencies import WorkspaceDirDep
from askui.chat.api.mcp_clients.dependencies import McpClientManagerManagerDep
from askui.chat.api.mcp_clients.manager import McpClientManagerManager
from askui.chat.api.messages.dependencies import MessageServiceDep, MessageTranslatorDep
from askui.chat.api.messages.service import MessageService
from askui.chat.api.messages.translator import MessageTranslator
from askui.chat.api.messages.chat_history_manager import ChatHistoryManager
from askui.chat.api.messages.dependencies import ChatHistoryManagerDep

from .service import RunService


def get_runs_service(
workspace_dir: Path = WorkspaceDirDep,
assistant_service: AssistantService = AssistantServiceDep,
chat_history_manager: ChatHistoryManager = ChatHistoryManagerDep,
mcp_client_manager_manager: McpClientManagerManager = McpClientManagerManagerDep,
message_service: MessageService = MessageServiceDep,
message_translator: MessageTranslator = MessageTranslatorDep,
) -> RunService:
"""Get RunService instance."""
return RunService(
base_dir=workspace_dir,
assistant_service=assistant_service,
mcp_client_manager_manager=mcp_client_manager_manager,
message_service=message_service,
message_translator=message_translator,
chat_history_manager=chat_history_manager,
)


Expand Down
Loading