From 483b7dc7eaaf763cf3ea3fd1c4a624c7843731cd Mon Sep 17 00:00:00 2001 From: liubiren Date: Fri, 7 Aug 2026 20:22:14 +0800 Subject: [PATCH] 1 --- agent/application/pages/conversation.py | 2 +- agent/application/states/conversation.py | 143 ++++++++------ agent/application/states/database.py | 230 ++++++++++++++--------- agent/application/states/models.py | 212 ++++++++++++--------- agent/application/tasks/__init__.py | 1 + agent/application/tasks/book_flight.py | 2 +- agent/database.db | Bin 126976 -> 131072 bytes 7 files changed, 353 insertions(+), 237 deletions(-) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index ef7ccb8..a781b73 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -25,7 +25,7 @@ def conversation_history_item_showing( # 高亮:若为当前会话或显示更多则高亮 highlight: bool = ( - conversation.id == ConversationState.shown_more_conversation_id + conversation.id == ConversationState.is_conversation_history_item_more_button_shown ) | (conversation.id == ConversationState.conversation_id) return rx.list.item( diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 912c5ff..bad1f3c 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -3,6 +3,7 @@ 会话状态 """ from typing import AsyncGenerator +from datetime import datetime from pydantic_ai.messages import ( FunctionToolCallEvent, @@ -24,15 +25,28 @@ import reflex as rx from application.states.database import DatabaseState from application.states.models import ( Conversation, - Dialog, - TaskNodeResultEvent, - Thought, - thoughts_to_dict, + ConversationHistoryItem, usage_to_dict, ) from application.tasks import run_stream_events +def format_conversation_history_item_created_at(created_at: datetime) -> str: + """ + 格式化会话历史项创建日期时间 + :param created_at: 创建日期时间 + :return: 格式化后的日期时间字符串 + """ + match (datetime.now().date() - created_at.date()).days: + case 0: + formatted_created_at = f"{created_at.strftime('%H:%M')}" + case 1: + formatted_created_at = f"昨天 {created_at.strftime('%H:%M')}" + case _: + formatted_created_at = created_at.strftime("%Y-%m-%d %H:%M") + return formatted_created_at + + class ConversationState(rx.State): """ 会话状态 @@ -40,70 +54,86 @@ class ConversationState(rx.State): # 当前用户唯一标识 user_id: str = "" - # 键为会话唯一标识,值为会话实例的会话字典 - conversations: dict[str, Conversation] = {} # 按照会话唯一标识顺序排序 - # 当前会话唯一标识 - conversation_id: str = "" + # 当前用户的会话字典(私有变量) + # 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用 + _conversations: dict[str, Conversation] = {} # 键为会话唯一标识,值为会话实例 + # 激活会话唯一标识 + actived_conversation_id: str = "" - # 会话历史展示状态,True表示展示,False表示隐藏 + # 显示会话历史,True 表示显示,False 表示隐藏 is_conversation_history_shown: bool = False - # 显示更多的会话唯一标识 - shown_more_conversation_id: str = "" + # 会话历史项显示悬停气泡,True 表示显示,False 表示隐藏 + is_conversation_history_item_popover_shown: bool = False # 用户提示词 user_prompt: str = "" - # 数据库状态(私有变量,不予序列化) + # 当前数据库状态(私有变量) _db_state: DatabaseState | None = None async def get_db_state(self) -> DatabaseState: """ - 获取数据库状态 - :return: 数据库状态 + 获取当前数据库状态 + :return: 当前数据库状态 """ + # 若数据库状态为 None 则获取数据库状态,否则直接返回当前数据库状态 if not self._db_state: self._db_state = await self.get_state(DatabaseState) return self._db_state - async def resume(self, user_id: str) -> None: + async def resume_conversation_state(self, user_id: str) -> None: """ - 恢复当前用户会话状态 + 恢复当前用户的会话状态 :param user_id: 用户唯一标识 :return: None """ - self.user_id = user_id + self.user_id = user_id.strip() if not self.user_id: return - # 获取数据库状态 + # 获取当前数据库状态 db_state = await self.get_db_state() - # 获取会话字典 - self.conversations = await db_state.get_conversations(user_id=self.user_id) - # 若会话字典为空则先创建会话记录再在会话字典中添加会话实例 - if not self.conversations: - self.conversations.update( - await db_state.create_conversations_record(user_id=self.user_id) + # 获取当前用户的会话字典 + self._conversations = await db_state.get_conversations(self.user_id) + # 若当前用户的会话字典为空则先创建会话记录再添加会话实例 + if not self._conversations: + self._conversations.update( + await db_state.create_conversation_record(self.user_id) ) - # 将最后一个会话作为当前会话并更新会话唯一标识 - self.conversation_id = next(reversed(self.conversations.keys())) + # 将最后一个会话的唯一标识作为激活会话唯一标识 + self.actived_conversation_id = next(reversed(self._conversations.keys())) @rx.event def toggle_conversation_history_shown(self) -> None: """ - 切换会话历史展示状态,用于前端是否渲染会话历史 + 显示 / 隐藏会话历史 :return: None """ self.is_conversation_history_shown = not self.is_conversation_history_shown - @rx.event - def set_shown_more_conversation_id( - self, conversation_id: str, is_shown: bool - ) -> None: + @rx.var + def conversation_history_items(self) -> list[ConversationHistoryItem]: """ - 设置显示更多的会话唯一标识 + 获取会话历史项列表(按照会话创建日期时间降序排序) + :return: 会话历史项列表 + """ + return [ + ConversationHistoryItem( + description=conversation.description, + created_at=format_conversation_history_item_created_at( + conversation.created_at + ), + ) + for conversation in reversed(self._conversations.values()) + ] + + @rx.event + def set_conversation_history_item_popover_shown(self, is_shown: bool) -> None: + """ + 设置会话历史项悬停气泡显示 / 隐藏 :return: None """ - self.shown_more_conversation_id = conversation_id if is_shown else "" + self.is_conversation_history_item_popover_shown = is_shown @rx.event async def delete_conversation(self, conversation_id: str) -> None: @@ -112,30 +142,29 @@ class ConversationState(rx.State): :param conversation_id: 需删除的会话唯一标识 :return: None """ - # 获取数据库状态 + # 获取当前数据库状态 db_state = await self.get_db_state() - # 先设置会话记录为已删除再在会话字典中删除会话实例 - await db_state.delete_conversations_record(conversation_id=conversation_id) - del self.conversations[conversation_id] + await db_state.delete_conversation_record(conversation_id) + del self._conversations[conversation_id] - # 删除后,若会话字典为空则先创建会话记录再添加会话实例 - if not self.conversations: - self.conversations.update( - await db_state.create_conversations_record(user_id=self.user_id) + # 删除后,若当前用户的会话字典为空则创建会话 + if not self._conversations: + self._conversations.update( + await db_state.create_conversation_record(self.user_id) ) - # 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识 - if self.conversation_id not in self.conversations: - self.conversation_id = next(reversed(self.conversations.keys())) + # 删除后,若激活会话唯一标识不存在则将最后一个会话的唯一标识作为激活会话唯一标识 + if self.actived_conversation_id not in self._conversations: + self.actived_conversation_id = next(reversed(self._conversations.keys())) @rx.event - def switch_conversation(self, conversation_id: str) -> None: + def set_actived_conversation(self, conversation_id: str) -> None: """ - 切换会话 - :param conversation_id: 需切换的会话唯一标识 + 设置激活会话 + :param conversation_id: 会话唯一标识 :return: None """ - self.conversation_id = conversation_id + self.actived_conversation_id = conversation_id @rx.event async def create_conversation(self) -> None: @@ -143,16 +172,14 @@ class ConversationState(rx.State): 创建会话 :return: None """ - # 获取数据库状态 + # 获取当前数据库状态 db_state = await self.get_db_state() # 创建会话记录再添加会话实例 - self.conversations.update( - await db_state.create_conversations_record( - user_id=self.user_id, description="新会话" - ) + self._conversations.update( + await db_state.create_conversation_record(self.user_id) ) - # 将最后一个会话作为当前会话并更新会话唯一标识 - self.conversation_id = next(reversed(self.conversations.keys())) + # 将最后一个会话的唯一标识作为激活会话唯一标识 + self.actived_conversation_id = next(reversed(self._conversations.keys())) @rx.event def set_user_prompt(self, user_prompt: str) -> None: @@ -164,13 +191,13 @@ class ConversationState(rx.State): self.user_prompt = user_prompt.strip() @rx.var - def is_user_prompt_sending_disabled(self) -> bool: + def is_user_prompt_send_button_disabled(self) -> bool: """ - 用户提示词发送禁用状态 + 用户提示词发送按钮不可点击 :return: bool """ # 当前会话 - conversation = self.conversations.get(self.conversation_id) + conversation = self._conversations.get(self.actived_conversation_id) if not conversation: return True return conversation.is_running or not self.user_prompt diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 85ba999..4ba6aa6 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -12,17 +12,29 @@ import reflex as rx from sqlalchemy import desc from sqlmodel import Field, JSON, SQLModel, select, update -from application.states.models import Conversation, Dialog +from application.states.models import ( + Conversation, + TaskType, + TaskStatus, + RunStatus, + MessageType, + Task, + Run, + Message, + deps_to_object, + usage_to_object, + usage_limits_to_object, + +) -class CaptchaRecord(SQLModel, table=True): +class VerificationCodeTable(SQLModel, table=True): """ - 验证码记录 - 邮箱、验证码和失效时间联合作为主键 + 验证码表 """ email: str = Field(..., primary_key=True, description="邮箱") - captcha: str = Field( + verification_code: str = Field( default_factory=lambda: "".join(choices("0123456789", k=6)), primary_key=True, description="验证码", @@ -40,13 +52,13 @@ class CaptchaRecord(SQLModel, table=True): expired_at: datetime = Field( default_factory=lambda: datetime.now() + timedelta(minutes=30), primary_key=True, - description="失效时间", + description="过期时间", ) -class UserRecord(SQLModel, table=True): +class UserTable(SQLModel, table=True): """ - 用户记录 + 用户表 """ id: str = Field( @@ -57,18 +69,18 @@ class UserRecord(SQLModel, table=True): email: str = Field(..., index=True, description="邮箱") -class ConversationRecord(SQLModel, table=True): +class ConversationTable(SQLModel, table=True): """ - 会话记录 + 会话表 """ id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, - description="会话记录唯一标识", + description="会话唯一标识", ) user_id: str = Field(..., index=True, description="用户唯一标识") - description: str = Field(..., description="会话描述") + description: str = Field(default="新会话", description="会话描述") is_deleted: bool = Field( default=False, index=True, @@ -79,51 +91,67 @@ class ConversationRecord(SQLModel, table=True): ) -class DialogRecord(SQLModel, table=True): +class TaskTable(SQLModel, table=True): """ - 对话记录 + 任务表 """ id: str = Field( - ..., + default_factory=lambda: str(uuid7()), primary_key=True, - description="对话唯一标识", + description="任务唯一标识", ) conversation_id: str = Field(..., index=True, description="会话唯一标识") - user_prompt: str = Field(..., description="用户提示词") - thoughts: dict[int, Any] = Field( - default_factory=dict, sa_type=JSON, description="思考列表" + type: TaskType = Field(default=TaskType.CHAT, description="任务类型") + status: TaskStatus = Field(default=TaskStatus.NONE, description="任务状态") + deps: dict[str, Any] = Field( + default_factory=dict, sa_type=JSON, description="任务依赖项" ) - result_output: str = Field(default="", description="结果输出") usage: dict[str, Any] = Field( - default_factory=dict, sa_type=JSON, description="使用量" + default_factory=dict, sa_type=JSON, description="任务使用量" + ) + usage_limits: dict[str, Any] = Field( + default_factory=dict, sa_type=JSON, description="任务使用量限制" ) -class ResultRecord(SQLModel, table=True): +class RunTable(SQLModel, table=True): """ - 结果记录 + 运行表 """ - conversation_id: str = Field(primary_key=True, description="会话唯一标识") - dialog_id: str = Field(primary_key=True, description="对话唯一标识") - new_messages: str = Field(description="新增消息") + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="运行唯一标识", + ) + task_id: str = Field(..., index=True, description="任务唯一标识") + status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态") + usage: dict[str, Any] = Field( + default_factory=dict, sa_type=JSON, description="运行使用量" + ) + usage_limits: dict[str, Any] = Field( + default_factory=dict, sa_type=JSON, description="运行使用量限制" + ) -def format_at(at: datetime) -> str: +class MessageTable(SQLModel, table=True): """ - 格式化日期时间 - :param at: 日期时间 - :return: 格式化后的日期时间字符串 + 消息表 """ - match (datetime.now().date() - at.date()).days: - case 0: - formatted_at = f"{at.strftime('%H:%M')}" - case 1: - formatted_at = f"昨天 {at.strftime('%H:%M')}" - case _: - formatted_at = at.strftime("%Y-%m-%d %H:%M") - return formatted_at + + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="消息唯一标识", + ) + run_id: str = Field(..., index=True, description="运行唯一标识") + type: MessageType = Field(default=MessageType.USER_PROMPT, description="消息类型") + title: str = Field(default="", description="消息标题") + content: str = Field(default="", description="消息内容") + + + class DatabaseState(rx.State): @@ -131,48 +159,51 @@ class DatabaseState(rx.State): 数据库状态 """ - async def create_captcha_record(self, email: str) -> str: + async def create_verification_code_record(self, email: str) -> str: """ 创建验证码记录 :param email: 邮箱 - :return: 所创建记录的验证码 + :return: 验证码 """ async with rx.asession() as session: - # 前置操作:将该邮箱有效且未核验的验证码记录设置为无效 + # 先将该邮箱的有效、未核验的验证码记录设置为无效 await session.exec( - update(CaptchaRecord) + update(VerificationCodeTable) .where( - CaptchaRecord.email == email, # type: ignore - CaptchaRecord.is_valid == True, # type: ignore - CaptchaRecord.is_verified == False, # type: ignore + VerificationCodeTable.email == email, # type: ignore + VerificationCodeTable.is_valid == True, # type: ignore + VerificationCodeTable.is_verified == False, # type: ignore ) .values(is_valid=False) ) await session.flush() - # 创建记录 - record = CaptchaRecord( + # 创建验证码记录 + record = VerificationCodeTable( email=email, ) session.add(record) await session.commit() await session.refresh(record) - return record.captcha + return record.verification_code - async def verify_captcha(self, email: str, captcha: str) -> bool: + async def verify_verification_code( + self, email: str, verification_code: str + ) -> bool: """ 核验验证码 :param email: 邮箱 - :param captcha: 验证码 + :param verification_code: 验证码 :return: 是否核验成功,True 表示核验成功,False 表示核验失败(根据邮箱和验证码未查询到有效且未核验的记录,或失效) """ async with rx.asession() as session: + # 查询该邮箱验证码且有效、未核验、过期时间大于当前时间的验证码记录 result = await session.exec( - select(CaptchaRecord).where( - CaptchaRecord.email == email, - CaptchaRecord.captcha == captcha, - CaptchaRecord.is_valid == True, - CaptchaRecord.is_verified == False, - CaptchaRecord.expired_at > datetime.now(), + select(VerificationCodeTable).where( + VerificationCodeTable.email == email, + VerificationCodeTable.verification_code == verification_code, + VerificationCodeTable.is_valid == True, + VerificationCodeTable.is_verified == False, + VerificationCodeTable.expired_at > datetime.now(), ) ) record = result.first() @@ -187,17 +218,17 @@ class DatabaseState(rx.State): """ 创建用户记录 :param email: 邮箱 - :return: 创建用户记录的唯一标识 + :return: 用户唯一标识 """ async with rx.asession() as session: result = await session.exec( - select(UserRecord).where(UserRecord.email == email) + select(UserTable).where(UserTable.email == email) ) record = result.first() - # 若记录已存在则返回用户唯一标识,否则创建并返回所创建记录的用户唯一标识 + # 若用户记录已存在则返回用户唯一标识,否则先创建用户记录再返回用户唯一标识 if record: return record.id - record = UserRecord(email=email) + record = UserTable(email=email) session.add(record) await session.commit() await session.refresh(record) @@ -205,56 +236,69 @@ class DatabaseState(rx.State): async def get_conversations(self, user_id: str) -> dict[str, Conversation]: """ - 获取会话字典 + 获取当前用户的会话字典 :param user_id: 用户唯一标识 - :return: 会话字典 + :return: 当前用户的会话字典 """ records: dict[str, Conversation] = {} async with rx.asession() as session: result = await session.exec( - select(ConversationRecord, DialogRecord) - .outerjoin(DialogRecord, ConversationRecord.id == DialogRecord.conversation_id) # type: ignore + select(ConversationTable, RunTable, MessageTable) + .outerjoin(RunTable, RunTable.conversation_id == ConversationTable.id) # type: ignore + .outerjoin(MessageTable, MessageTable.run_id == RunTable.id) # type: ignore .where( - ConversationRecord.user_id == user_id, - ConversationRecord.is_deleted == False, + ConversationTable.user_id == user_id, + ConversationTable.is_deleted == False, + ) + .order_by( + ConversationTable.id, TaskTable.id, RunTable.id, MessageTable.id ) - .order_by(ConversationRecord.id, DialogRecord.id) ) - for conversation_record, dialog_record in result.all(): - record = records.setdefault( - conversation_record.id, + for ( + conversation_result, + run_result, + message_result, + ) in result.all(): + conversation_record = records.setdefault( + conversation_result.id, Conversation( - id=conversation_record.id, - description=conversation_record.description, - created_at=format_at(conversation_record.created_at), + id=conversation_result.id, + description=conversation_result.description, + created_at=conversation_result.created_at, ), ) - if not dialog_record: + if not run_result: continue - record.dialogs.update( - { - dialog_record.id: Dialog( - id=dialog_record.id, - user_prompt=dialog_record.user_prompt, - thoughts=dialog_record.thoughts, - result_output=dialog_record.result_output, - usage=dialog_record.usage, - ) - } + run_record = conversation_record.runs.setdefault( + run_result.id, + Run( + id=run_result.id, + status=run_result.status, + usage=usage_to_object(run_result.usage), + usage_limits=usage_limits_to_object(run_result.usage_limits), + ), + ) + if not message_result: + continue + run_record.messages.setdefault( + message_result.id, + Message( + id=message_result.id, + type=message_result.type, + title=message_result.title, + content=message_result.content, + ), ) return records - async def create_conversations_record( - self, user_id: str, description: str = "新会话" - ) -> dict[str, Conversation]: + async def create_conversation_record(self, user_id: str) -> dict[str, Conversation]: """ 创建会话记录 :param user_id: 用户唯一标识 - :param description: 会话描述,默认为"新会话" - :return: 键为会话唯一标识,值为会话实例的会话字典 + :return: 会话实例 """ async with rx.asession() as session: - record = ConversationRecord(user_id=user_id, description=description) + record = ConversationTable(user_id=user_id) session.add(record) await session.commit() await session.refresh(record) @@ -262,18 +306,18 @@ class DatabaseState(rx.State): record.id: Conversation( id=record.id, description=record.description, - created_at=format_at(record.created_at), + created_at=record.created_at, ) } - async def delete_conversations_record(self, conversation_id: str) -> None: + async def delete_conversation_record(self, conversation_id: str) -> None: """ 删除会话记录(逻辑删除) :param conversation_id: 指定会话唯一标识 :return: None """ async with rx.asession() as session: - record = await session.get(ConversationRecord, conversation_id) + record = await session.get(ConversationTable, conversation_id) if not record: return record.is_deleted = True diff --git a/agent/application/states/models.py b/agent/application/states/models.py index 8f3f0b1..6edbfb7 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -4,6 +4,7 @@ """ from enum import StrEnum from typing import Any +from datetime import datetime from pydantic import BaseModel, Field, TypeAdapter from pydantic_ai import RunUsage @@ -11,44 +12,136 @@ from pydantic_ai._uuid import uuid7 from pydantic_ai.usage import UsageLimits -class Thought(BaseModel): +class MessageType(StrEnum): """ - 思考类 + 消息类型枚举 """ - type: str = Field(..., description="思考类型") - content: str = Field(..., description="思考内容") + USER_PROMPT = "user_prompt" + THINKING = "thinking" + AGENT_RUN_RESULT_OUTPUT = "agent_run_result_output" -# 思考领域模型类型适配器 -ThoughtsAdapter = TypeAdapter(dict[int, Thought]) - - -def thoughts_to_dict(thoughts: dict[int, Thought]) -> dict: +class Message(BaseModel): """ - Thought 转为字典 - """ - return ThoughtsAdapter.dump_python(thoughts) - - -class Dialog(BaseModel): - """ - 对话类 + 消息类 """ - id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识") - user_prompt: str = Field(default="", description="用户提示词") - thoughts: dict[int, Thought] = Field(default_factory=dict, description="思考列表") - result_output: str = Field(default="", description="结果输出") - usage: dict[str, Any] = Field(default_factory=dict, description="对话使用量") + id: str = Field(default_factory=lambda: str(uuid7()), description="消息唯一标识") + type: MessageType = Field(..., description="消息类型") + title: str = Field(default="", description="消息标题") + content: str = Field(default="", description="消息内容") + is_component_expanded: bool = Field( + default=True, description="展开组件,True 表示展开,False 表示折叠" + ) + + +class RunStatus(StrEnum): + """ + 运行状态枚举 + """ + + RUNNING = "running" + FINISHED = "finished" + + +class Run(BaseModel): + """ + 运行类 + """ + + id: str = Field(default_factory=lambda: str(uuid7()), description="运行唯一标识") + messages: dict[str, Message] = Field(default_factory=dict, description="消息字典") + status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态") + usage: RunUsage = Field(default=RunUsage(), description="运行使用量") + usage_limits: UsageLimits | None = Field(default=None, description="运行使用量限制") is_thinking: bool = Field( default=False, description="正在思考,True 表示正在思考,False 表示未正在思考" ) - is_expanded: bool = Field( - default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" + + +class TaskType(StrEnum): + """ + 任务类型枚举 + """ + + CHAT = "聊天" + BOOK_FLIGHT = "预定航班" + + +class TaskStatus(StrEnum): + """ + 任务状态枚举 + """ + + NONE = "none" + + +class Deps(BaseModel): + """ + 依赖项类 + """ + + ... + + +class Task(BaseModel): + """ + 任务类 + """ + + type: TaskType = Field(default=TaskType.CHAT, description="任务类型") + status: TaskStatus = Field(default=TaskStatus.NONE, description="任务状态") + deps: Deps | None = Field(default=None, description="任务依赖项") + usage: RunUsage = Field(default=RunUsage(), description="任务使用量") + usage_limits: UsageLimits | None = Field( + default=None, + description="任务使用量限制", ) +class Conversation(BaseModel): + """ + 会话类 + """ + + id: str = Field(default_factory=lambda: str(uuid7()), description="会话唯一标识") + runs: dict[str, Run] = Field(default_factory=dict, description="运行字典") + description: str = Field(default="新会话", description="会话描述") + created_at: datetime = Field(..., description="会话创建日期时间") + + +class ConversationHistoryItem(BaseModel): + """ + 会话历史项类 + """ + + description: str = Field(..., description="会话描述") + created_at: str = Field(..., description="会话创建日期时间") + + +# Dpes 适配器 +DepsAdapter = TypeAdapter(Deps) + + +def deps_to_object(deps: dict[str, Any]) -> Deps | None: + """ + Deps 转为对象 + """ + if not deps: + return None + return DepsAdapter.validate_python(deps) + + +def deps_to_dict(deps: Deps | None) -> dict[str, Any]: + """ + Deps 转为字典 + """ + if not deps: + return {} + return DepsAdapter.dump_python(deps) + + # Usage 适配器 UsageAdapter = TypeAdapter(RunUsage) @@ -57,9 +150,9 @@ def usage_to_object(usage: dict[str, Any]) -> RunUsage: """ Usage 转为对象 """ - if isinstance(usage, dict) and usage: - return UsageAdapter.validate_python(usage) - return RunUsage() + if not usage: + return RunUsage() + return UsageAdapter.validate_python(usage) def usage_to_dict(usage: RunUsage) -> dict[str, Any]: @@ -69,72 +162,23 @@ def usage_to_dict(usage: RunUsage) -> dict[str, Any]: return UsageAdapter.dump_python(usage) -# UsageLimits 适配器 +# UsageLimits 适配器 UsageLimitsAdapter = TypeAdapter(UsageLimits) -def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits: +def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits | None: """ UsageLimits 转为对象 """ - if isinstance(usage_limits, dict) and usage_limits: - return UsageLimitsAdapter.validate_python(usage_limits) - return UsageLimits(request_limit=5) + if not usage_limits: + return None + return UsageLimitsAdapter.validate_python(usage_limits) -def usage_limits_to_dict(usage_limits: UsageLimits) -> dict[str, Any]: +def usage_limits_to_dict(usage_limits: UsageLimits | None) -> dict[str, Any]: """ UsageLimits 转为字典 """ + if not usage_limits: + return {} return UsageLimitsAdapter.dump_python(usage_limits) - - -class TaskType(StrEnum): - """ - 任务类型枚举 - """ - - BOOK_FLIGHT = "book_flight" - - -class Task(BaseModel): - """ - 任务类 - """ - - id: str = Field(default_factory=lambda: str(uuid7()), description="任务唯一标识") - type: TaskType = Field(..., description="任务类型") - node: str = Field(default="", description="任务节点") - deps: Any = Field(default=None, description="任务依赖项") - usage: dict[str, Any] = Field(default_factory=dict, description="任务使用量") - usage_limits: dict[str, Any] = Field( - default_factory=dict, description="任务使用量限制" - ) - - -class Conversation(BaseModel): - """ - 会话类 - """ - - id: str = Field(default_factory=lambda: str(uuid7()), description="会话唯一标识") - description: str = Field(default="新会话", description="会话描述") - is_running: bool = Field( - default=False, - description="会话正在运行,True 表示正在运行,False 表示未正在运行", - ) - dialogs: dict[str, Dialog] = Field(default_factory=dict, description="对话字典") - task: Task | None = Field(default=None, description="会话任务") - created_at: str = Field( - ..., - description="创建时间", - ) - - -class TaskNodeResultEvent(BaseModel): - """ - 任务节点结果事件类 - """ - - task: Task | None = Field(..., description="任务实例") - content: str = Field(default="", description="任务结果内容") diff --git a/agent/application/tasks/__init__.py b/agent/application/tasks/__init__.py index bd9108f..e9dd338 100644 --- a/agent/application/tasks/__init__.py +++ b/agent/application/tasks/__init__.py @@ -47,6 +47,7 @@ async def run_stream_events( user_prompt=user_prompt, message_history=message_history, ): + print(event) yield event else: diff --git a/agent/application/tasks/book_flight.py b/agent/application/tasks/book_flight.py index 4e13ec1..886b01a 100644 --- a/agent/application/tasks/book_flight.py +++ b/agent/application/tasks/book_flight.py @@ -67,7 +67,7 @@ flight_details_extraction_agent = Agent( ) -@flight_search_agent.tool +@flight_search_agent.tool(name="提取所有航班详情") async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]: """ 工具:提取所有航班详情 diff --git a/agent/database.db b/agent/database.db index 9ef11eb246db2361c9b6a6d110372f4234cf0cc3..174c339cb93e9a95d654500c56804123056ea44e 100644 GIT binary patch delta 7388 zcmeHMYm8Lo8J>^1EG+CEu+Uw=$`n_M4QKf7m$4OzX^CJf3PTj4@||03q-=quY7{|f zn`(j!ozxSpMu~|wP3pSc#1-|2{%TAct!bk6N2NgpLR&BW(X`QqzUR#B?CdTxNHhK` z36ssY-<ks@1edWWGg(rc zQ!RI8YY(Z~;{(+{-n(wqu6%MZr)HXoq{{=tTqTL;TEVf)6_uEB&ls*6Pj!-~vN9JX zP8Il4J+%Lh-n=t636kn?E*l&?6S78YbbM4ufw^48dy=Pp9q4h|N& zE0x9fU47rugLkAsN__V`EL=rB;d)pLmzof=;H!vezj3Ec;`fAhw|`r8_{Fgt=bFS{ z9YJ^r6Pm9oE+5=G^n<-aw+;>79`4+;yj;}6R1d#&u=??_MVsou)k7~mQu$FaH{)IK8cq#-Xy5guA#Ugs z5^)=8B24{VZL^)eYi#(%7>)54e%rMa0^V2XhR?3v6B*|nX9R$m!m{@SB>$=2>ao`^ zp9)SxEq4_O8Puj9xFYm438gXC=TpJiCPB^M)NY&br7*d?;aD5zb196kI(u1le|?&mP~hu;83Xs-g%b`A_E)=Wos_ z=fvodujR(py#Lf(4!`Lyz{j$rfG*C#$C9Kp7tMl?g(%Hy&+&{i=7dgP?`yqx_qsjD zA+N9XRFD6sd;fU|HZ?T4(=O%T$yMsibsj>hyWLc{`)5{e;ldx+4n56Jn^_jDiLo0= zX$LWQTy^yLim|KCzPkNO5a9y)EHiIKcXl?fG1lmj)Bk6yCX((fTm8Q4;pb33Kl{_-8^hi7km$S>q8mB%KCuWh5B3BQ@>t8PElmrol*b?t|X7pTpcma!~TUJ}$O%Hk&< zL1NmmAv8vtkh-MalOm7=21+L4!sUi>8`>xcRrSe_mM%-KP(~~Wn#j7wm&B!1s>m8I z2>r17<{28Vz6Gs}w{Jy1>yJDi8%6@|g#lNDc;F|rHyL+tL;JE6llZ}HXjP7Y=!lPP zL&qi!6D6&2V%YXA=%KE<8}Z{C&^HDmi?ttmzALm0T^>@XM}@HqHbgiIBH(ozAKid1 zju&5x7Ie_5?XCT}`37VrjRyfH41q@Y$t~!bDaVp()yZIA7N30r<;Qay(b`%49%Uh+ zG-5Ph-1m*=QRNAv<0k7kXt|OyOmNa{UHzDIY*Hvdo)hFVA2g@3t`0rr`o>p&M46@7 zRF8hN1R{H`JCZTO{LohaaE3LYAmU}4(baP{efv6h?FIrcynQoTpJxK^TOO~z0e!jO z_dyG3rFmdOkOx|G!2?2Vf|PsWpo0Edk0V$rO4t}JM2d|$HEh6f{KN)S9*8*ibs&A$ zC=$4wXz4;+tQ&AfeV=+Z^nyurbjHssJ!PS+2N%fM}*8ngTqlLB1zc7*{K3A)@i< zA+#fYc|E!!!3Qf7UM94}6y{Q=s88ZwKd!~!NSGdc5`mhw%7y9&QuM8HrS@3pg;s#` zP4TZuA`fVs3y}Nvuq^!ZNIFb zXX;HgLsq1OwTu9zS$)E;*Y%y3qE805X}c;wTWaeIF& z52j&VL07>N05C!9wS*+cl;Vbx>qO*XzFGr|ZOr0rc4dt_ zZT36UNnRw2cF900@s#B2^1FUafy|G--Iy9CPKBw>lr4()LhB&SMl;E`PE07kj#exWckXG|<-CXY1~ zV5jBny#ABJhd&s8_~ZizPCoX)>7V}U)bOuQk34{9FY0oFVK})s%J`8@X?fv0g*Dv>#6L5Z>MvjWD-^*@MjMarCpxDpdNbC zwH{P1%%DLCSZ*wDmUT|Dwsv~_t!vSWY`o=qv|xPEMucIJ(kcK`1SXaxO}R0XYo3~? f$+2WG=p;L}4tkL8)Xuv|X*rEStzMaI*O&em#Zc@w literal 126976 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