From 3f3e734c9612649fc89353cee0894a8df0eb03f8 Mon Sep 17 00:00:00 2001 From: liubiren Date: Thu, 17 Sep 2026 18:37:53 +0800 Subject: [PATCH] 1 --- agent/application/application.py | 8 +- agent/application/pages/conversation.py | 34 ++-- agent/application/states/conversation.py | 38 ++++- agent/application/states/models.py | 3 +- .../workshop/bank_customer_service.py | 149 ++++++++++++++++++ agent/application/workshop/book_flight.py | 5 +- agent/application/workshop/database.py | 30 ++++ agent/application/workshop/events.py | 2 + agent/database.db | Bin 237568 -> 266240 bytes 9 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 agent/application/workshop/bank_customer_service.py create mode 100644 agent/application/workshop/database.py diff --git a/agent/application/application.py b/agent/application/application.py index 8175713..afe4427 100644 --- a/agent/application/application.py +++ b/agent/application/application.py @@ -3,7 +3,10 @@ 应用主入口 """ import reflex as rx + from application.pages.index import index +from application.workshop.database import connection_pool_init + app = rx.App( theme=rx.theme(color_mode="light"), @@ -36,5 +39,8 @@ app = rx.App( }, # 覆盖占位符样式 }, # 自定义主题颜色 ) # 此处变量名需使用 app ,具体原因目前尚不清楚 -# 注册首页路由 + +# 添加页面:首页 app.add_page(component=index(), route="/") +# 注册声明周期任务:连接池初始化 +app.register_lifespan_task(connection_pool_init) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index a19e48a..3803022 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -226,6 +226,24 @@ def conversation_history( ) +def workflow_button(workflow_type: WorkflowType): + """ + 工作流按钮 + :param workflow_type: 工作类型 + :return: rx.Component + """ + return rx.box( + workflow_type.value, + # 点击事件:设置工作流 + on_click=lambda: ConversationState.set_workflow(workflow_type), + padding="10px 16px", + background_color="var(--prismui-background-color-6)", + border_radius="var(--prismui-border-radius-9)", + color="var(--prismui-color-10)", + cursor="pointer", + ) + + def use_guidance() -> rx.Component: """ 使用指引 @@ -276,18 +294,10 @@ def use_guidance() -> rx.Component: font_weight="var(--prismui-font-weight-3)", ), rx.hstack( - rx.box( - "预定航班", - # 点击事件:设置工作流为预定航班 - on_click=lambda: ConversationState.set_workflow( - WorkflowType.BOOK_FLIGHT - ), - padding="10px 16px", - background_color="var(--prismui-background-color-6)", - border_radius="var(--prismui-border-radius-9)", - color="var(--prismui-color-10)", - cursor="pointer", - ), + # 预定航班按钮 + workflow_button(WorkflowType.BOOK_FLIGHT), + # 银行客服按钮 + workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE), flex_wrap="wrap", align_items="center", gap="12px", diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index f6411bc..7b96929 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -357,6 +357,8 @@ class ConversationState(rx.State): message_history=message_history, usage=usage, ) + + # 预定航班 case WorkflowType.BOOK_FLIGHT: from application.workshop.book_flight import ( run_stream_events, @@ -371,6 +373,30 @@ class ConversationState(rx.State): deferred_tool_results=deferred_tool_results, ) + # 银行客户服务 + case WorkflowType.BANK_CUSTOMER_SERVICE: + from application.workshop.bank_customer_service import ( + run_stream_events, + Deps, + ) + from application.workshop.database import connection_pool + + # 若连接池为空则直接返回 + if not connection_pool: + return + + deps = Deps( + account_id=conversation.workflow.deps["account_id"], + connection_pool=connection_pool, + ) + # 运行并流式输出事件 + stream_events = run_stream_events( + deps=deps, + user_prompt=user_prompt, + message_history=message_history, + usage=usage, + ) + # 消息列表 messages: list[Message] = [] # 获取运行流式输出事件 @@ -540,8 +566,8 @@ class ConversationState(rx.State): messages.append(message) else: - # 若工作流类型非对话则设置为对话 - if conversation.workflow.type != WorkflowType.TALK: + # 若工作流类型为预定航班则设置为对话 + if conversation.workflow.type == WorkflowType.BOOK_FLIGHT: conversation.workflow = Workflow() # 将正在运行设置为否 conversation.is_running = False @@ -634,6 +660,7 @@ class ConversationState(rx.State): return match type: + # 预定航班 case WorkflowType.BOOK_FLIGHT: from application.workshop.book_flight import deps @@ -644,6 +671,13 @@ class ConversationState(rx.State): f"帮我查询并预定在 {workflow.deps.date.strftime('%Y-%m-%d')} 从 {workflow.deps.origin_airport_code} 到 {workflow.deps.destination_airport_code} 的航班" ) + # 银行客户服务 + case WorkflowType.BANK_CUSTOMER_SERVICE: + # 初始化银行客户服务工作流 + workflow = Workflow(type=type, deps={"account_id": self.user_id}) + # 设置用户提示词 + self.set_user_prompt(f"帮我查询账户余额") + case _: # 初始化对话工作流 workflow = Workflow() diff --git a/agent/application/states/models.py b/agent/application/states/models.py index bd8857b..0a04d1f 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -58,8 +58,9 @@ class WorkflowType(StrEnum): 工作类型枚举 """ - TALK = "talk" + TALK = "对话" BOOK_FLIGHT = "预定航班" + BANK_CUSTOMER_SERVICE = "银行客户服务" class Workflow(BaseModel): diff --git a/agent/application/workshop/bank_customer_service.py b/agent/application/workshop/bank_customer_service.py new file mode 100644 index 0000000..a4dc955 --- /dev/null +++ b/agent/application/workshop/bank_customer_service.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" +银行客户服务 +""" +from typing import Any, AsyncGenerator + +from pydantic import BaseModel, Field +from pydantic_ai import ( + Agent, + Capability, + DeferredToolResults, + ModelMessage, + RunContext, + RunUsage, + UsageLimits, +) +from pydantic_ai.messages import ( + AgentStreamEvent, + PartStartEvent, + ToolCallPart, + FunctionToolResultEvent, + ToolReturnPart, +) +from pydantic_ai.run import AgentRunResultEvent, AgentRunResult + +from application.workshop.models import ( + DEEPSEEK_V4_FLASH_MODEL, + DEEPSEEK_V4_FLASH_MODEL_SETTINGS, +) +from asyncpg import Pool +from application.workshop.events import AgentRuntimeEvent + + +class Deps(BaseModel): + """ + 依赖类 + """ + + model_config = {"arbitrary_types_allowed": True} # 允许任意类型 + + account_id: str = Field(..., description="账户唯一标识") + connection_pool: Pool = Field(..., description="连接池") + + +class Output(BaseModel): + """ + 输出类 + """ + + response: str = Field(..., description="客服应答") + + +# 账户上下文能力 +account_context = Capability[Deps]( + id="account-context", + description="提供账户信息,用于客服应答", + defer_loading=False, # 延迟加载置为否,立即加载 +) + + +@account_context.instructions +async def query_account_name(ctx: RunContext[Deps]) -> str: + """ + 查询账户户名 + """ + async with ctx.deps.connection_pool.acquire() as conn: + account_name = await conn.fetchval( + "SELECT account_name FROM bank_customer_service WHERE account_id = $1", + ctx.deps.account_id, + ) + if account_name is None: + return "该账户不存在" + return f"该账户户名是 {account_name!r}" + + +@account_context.tool(name="查询账户余额") +async def query_account_balance(ctx: RunContext[Deps]) -> str: + """ + 查询账户余额 + """ + async with ctx.deps.connection_pool.acquire() as conn: + account_balance = await conn.fetchval( + "SELECT account_balance FROM bank_customer_service WHERE account_id = $1", + ctx.deps.account_id, + ) + if account_balance is None: + return "该账户不存在" + return f"该账户余额: {account_balance:.2f}" + + +# 智能体 +agent = Agent[Deps, Output]( + model=DEEPSEEK_V4_FLASH_MODEL, + model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + deps_type=Deps, + output_type=Output, + instructions="你是我方银行的客服智能体,为客户提供客服应答。", + capabilities=[account_context], +) + + +async def run_stream_events( + deps: Any, + user_prompt: str | None, + message_history: list[ModelMessage], + usage: RunUsage, + usage_limits: UsageLimits | None = None, + deferred_tool_results: DeferredToolResults | None = None, +) -> AsyncGenerator[AgentStreamEvent | AgentRunResultEvent[Output] | None,]: + """ + 运行并流式输出事件 + """ + if not isinstance(deps, Deps): + deps = Deps.model_validate(deps) + + async with agent.run_stream_events( + user_prompt=user_prompt, + deps=deps, + message_history=message_history, + usage=usage, + usage_limits=usage_limits, + deferred_tool_results=deferred_tool_results, + ) as events: + async for event in events: + match event: + case PartStartEvent( + part=part, + ): + match part: + case ToolCallPart(tool_name=tool_name): + if tool_name.startswith("final_result"): + continue + + case FunctionToolResultEvent(part): + match part: + case ToolReturnPart( + tool_name=tool_name, + ): + if tool_name.startswith("final_result"): + continue + + case AgentRunResultEvent(result=result): + match result: + case AgentRunResult(output=output): + match output: + case Output(response=response): + yield AgentRuntimeEvent(content=response) + + yield event diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index cc1c57a..d13e4a2 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -8,7 +8,6 @@ from typing import Any, AsyncGenerator, cast from pydantic import BaseModel, Field, field_validator from pydantic_ai import ( Agent, - AgentRunResult, ApprovalRequired, DeferredToolRequests, DeferredToolResults, @@ -25,7 +24,7 @@ from pydantic_ai.messages import ( ToolCallPart, ToolReturnPart, ) -from pydantic_ai.run import AgentRunResultEvent +from pydantic_ai.run import AgentRunResultEvent, AgentRunResult from application.workshop.events import AgentRuntimeEvent from application.workshop.models import ( @@ -348,7 +347,7 @@ async def run_stream_events( tool_call_id=tool_call_id, ) yield agent_runtime_event - + yield event # 智能体运行时依赖 event,原则上不可修改 case AgentRunResultEvent(result=result): diff --git a/agent/application/workshop/database.py b/agent/application/workshop/database.py new file mode 100644 index 0000000..867f5e2 --- /dev/null +++ b/agent/application/workshop/database.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +数据库模块 +""" +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +from asyncpg import Pool, create_pool + +# 连接池 +connection_pool: Pool | None = None + + +@asynccontextmanager +async def connection_pool_init() -> AsyncIterator[None]: + """ + 连接池初始化 + """ + global connection_pool + + # 创建连接池 + connection_pool = await create_pool( + "postgres://root:PG198752@liubiren.cloud:5201/database", + min_size=1, + max_size=5, + server_settings={"statement_timeout": "5000"}, + ) + yield + if connection_pool: + await connection_pool.close() diff --git a/agent/application/workshop/events.py b/agent/application/workshop/events.py index e3b5e57..5125dd2 100644 --- a/agent/application/workshop/events.py +++ b/agent/application/workshop/events.py @@ -13,4 +13,6 @@ class AgentRuntimeEvent(CustomEvent): 智能体运行时事件 """ + # tool_call_id 无需自定义,继承 CustomEvent 中 tool_call_id + content: str = "" diff --git a/agent/database.db b/agent/database.db index 18d31a94757fc219d9058990f4df254a5cbad0ac..c83cc2545ef143d54ef0cbd272020dde554e32d1 100644 GIT binary patch delta 14016 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 delta 872 zcmaKqUq}>D6vlVv&bm9!=rs#L3C(FA3U*-T?%tid8%aF{K@Sz84@KVDnUQkQ)Llws zTT5zt=#P4dg&=gHmq?RkAfbm6io%zOR0<)AAS_6-9tyIeQKNi{!+ki1bH49)4tHRz zYGAr@sIH8g_xXH@;cZPkH(zQicGY^RAlg8lsB9H&NeqU0YdwaG2i=a7Z-iVt;y6(= zZkWU%mZ=jZp=dhQDAtgw#jUt%U}~!-(TQrIXp-nLC+XNo2`fpON*%*gH!VUa)#7&4 zu;SEkbcC_JQ3fI1RE>zKtEQzpDn$mhv5|}!X1-4Hmp3;z>e~W-3 zQB>%D#f9C)o+Esr{Q{`v6G~yG9fWu#T)1=^Gg zlRlYAA4_+2*qyyfB8E_;u*U!=8W7e<);mU5Z(2!oW>TrJ(qngL?B2B3hBP*wtKHzH z@PFKndv2u{Avb@f=^qXInF{~`9(4l)F}N*GW2%DMzwmiCJU3{z=vOAMI+4;#_ zy#!~xKjJgs3j7K`!;f$gzJ;&gOE?Rk!kJR>J2$nphnuKiGY@uw17Ms5rf-yruX#3f zMf3;_RoAiO5t4{XDM_j^q*KepQ4_~en>+rnRbHQZ*2hMkDLfe5F#O;3+E&+d=VmoI zSOZ5^83~i$guSg6q^as7+yG!BpL#&8>|=rDh+AFtSXyOr#UCqRwR$bUZHJX_E3Q{` z%ZKEh@=dv4_J*IVb>&5w^BzAX%3$Siz0Bu*@@DCF`DO8@e^GcBoCf=Z{ek;s3w)j* OFBNTmDsO_m-M;|{coWqC