This commit is contained in:
parent
41a42808d2
commit
bc14d3aa16
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from application.agents.chat_agent import chat_agent
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"chat_agent",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
开放式对话智能体
|
||||||
|
"""
|
||||||
|
from pydantic_ai import Agent
|
||||||
|
from pydantic_ai.models.openai import OpenAIChatModel
|
||||||
|
from pydantic_ai.providers.openai import OpenAIProvider
|
||||||
|
|
||||||
|
instructions: str = """
|
||||||
|
# 角色
|
||||||
|
专业友好AI助手,结构化解答各类问题。
|
||||||
|
|
||||||
|
# 输出硬性规则
|
||||||
|
1. 全文强制标准Markdown,禁止纯文本;不要额外说明排版格式,直接输出内容;
|
||||||
|
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
|
||||||
|
3. 代码块用 ```语言名``` 包裹;
|
||||||
|
4. 重点内容标注 **粗体**/*斜体*;
|
||||||
|
5. 思考、工具日志仅输出文本,适配前端折叠面板,禁止输出HTML标签;
|
||||||
|
6. 内容分点拆分,排版整洁适配前端Markdown渲染。
|
||||||
|
|
||||||
|
# 行文要求
|
||||||
|
语言通俗,逻辑完整简洁,无多余废话。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 实例化智能体
|
||||||
|
chat_agent: Agent = Agent(
|
||||||
|
name="chat_agent",
|
||||||
|
model=OpenAIChatModel(
|
||||||
|
model_name="deepseek-v4-flash",
|
||||||
|
provider=OpenAIProvider(
|
||||||
|
base_url="https://tokenhub.tencentmaas.com/v1",
|
||||||
|
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
instructions=instructions,
|
||||||
|
capabilities=None,
|
||||||
|
output_type=str,
|
||||||
|
retries=1,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
生成产品需求文档智能体
|
||||||
|
"""
|
||||||
|
from pydantic_ai import Agent
|
||||||
|
from pydantic_ai.models.openai import OpenAIChatModel
|
||||||
|
from pydantic_ai.providers.openai import OpenAIProvider
|
||||||
|
|
||||||
|
instructions: str = """
|
||||||
|
# 角色
|
||||||
|
专业友好AI助手,结构化解答各类问题。
|
||||||
|
|
||||||
|
# 输出硬性规则
|
||||||
|
1. 全文强制标准Markdown,禁止纯文本;不要额外说明排版格式,直接输出内容;
|
||||||
|
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
|
||||||
|
3. 代码块用 ```语言名``` 包裹;
|
||||||
|
4. 重点内容标注 **粗体**/*斜体*;
|
||||||
|
5. 思考、工具日志仅输出文本,适配前端折叠面板,禁止输出HTML标签;
|
||||||
|
6. 内容分点拆分,排版整洁适配前端Markdown渲染。
|
||||||
|
|
||||||
|
# 行文要求
|
||||||
|
语言通俗,逻辑完整简洁,无多余废话。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 实例化智能体
|
||||||
|
chat_agent: Agent = Agent(
|
||||||
|
name="chat_agent",
|
||||||
|
model=OpenAIChatModel(
|
||||||
|
model_name="deepseek-v4-flash",
|
||||||
|
provider=OpenAIProvider(
|
||||||
|
base_url="https://tokenhub.tencentmaas.com/v1",
|
||||||
|
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
instructions=instructions,
|
||||||
|
capabilities=None,
|
||||||
|
output_type=str,
|
||||||
|
retries=1,
|
||||||
|
)
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_ai._uuid import uuid7
|
from pydantic_ai._uuid import uuid7
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class Thought(BaseModel):
|
class Thought(BaseModel):
|
||||||
|
|
@ -24,7 +25,7 @@ class Dialog(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识")
|
id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识")
|
||||||
user_prompt: str = Field(..., description="用户提示词")
|
user_prompt: str = Field(default="", description="用户提示词")
|
||||||
thoughts: Dict[int, Thought] = Field(default_factory=dict, description="思考列表")
|
thoughts: Dict[int, Thought] = Field(default_factory=dict, description="思考列表")
|
||||||
result_output: str = Field(default="", description="结果输出")
|
result_output: str = Field(default="", description="结果输出")
|
||||||
is_thinking: bool = Field(
|
is_thinking: bool = Field(
|
||||||
|
|
@ -50,3 +51,12 @@ class Conversation(BaseModel):
|
||||||
...,
|
...,
|
||||||
description="创建时间",
|
description="创建时间",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskType(StrEnum):
|
||||||
|
"""
|
||||||
|
任务类型枚举
|
||||||
|
"""
|
||||||
|
|
||||||
|
CHAT = "chat"
|
||||||
|
GENERATING_PRD = "generating_prd"
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,8 @@ def greeting_showing() -> rx.Component:
|
||||||
),
|
),
|
||||||
rx.hstack(
|
rx.hstack(
|
||||||
rx.box(
|
rx.box(
|
||||||
"智能客服",
|
"生成产品需求文档",
|
||||||
|
on_click=ConversationState.generate_prd,
|
||||||
padding="10px 16px",
|
padding="10px 16px",
|
||||||
background_color="var(--prismui-background-color-6)",
|
background_color="var(--prismui-background-color-6)",
|
||||||
border_radius="var(--prismui-border-radius-9)",
|
border_radius="var(--prismui-border-radius-9)",
|
||||||
|
|
@ -372,96 +373,122 @@ def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
|
||||||
is_expanded = dialog.is_expanded
|
is_expanded = dialog.is_expanded
|
||||||
|
|
||||||
return rx.vstack(
|
return rx.vstack(
|
||||||
rx.hstack(
|
# 用户提示词
|
||||||
rx.spacer(),
|
rx.cond(
|
||||||
rx.vstack(
|
dialog.user_prompt,
|
||||||
# 用户提示词
|
rx.hstack(
|
||||||
rx.text(
|
rx.spacer(),
|
||||||
dialog.user_prompt,
|
rx.vstack(
|
||||||
max_width="600px",
|
|
||||||
padding="12px 16px",
|
|
||||||
background_color="var(--prismui-background-color-3)",
|
|
||||||
border_radius="var(--prismui-border-radius-3)",
|
|
||||||
line_height="1.5",
|
|
||||||
word_wrap="break-word",
|
|
||||||
word_break="break-all",
|
|
||||||
white_space="pre-line",
|
|
||||||
),
|
|
||||||
rx.hstack(
|
|
||||||
# 再次发送
|
|
||||||
rx.box(
|
|
||||||
rx.icon(
|
|
||||||
"rotate-ccw",
|
|
||||||
width="14px",
|
|
||||||
height="14px",
|
|
||||||
color="var(--prismui-color-2)",
|
|
||||||
),
|
|
||||||
padding="4px",
|
|
||||||
border_radius="var(--prismui-border-radius-1)",
|
|
||||||
cursor="pointer",
|
|
||||||
style={
|
|
||||||
"_hover": {
|
|
||||||
"background_color": "var(--prismui-background-color-2)",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
width="100%",
|
|
||||||
margin_top="8px",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
align_items="flex-start",
|
|
||||||
gap="4px",
|
|
||||||
width="100%",
|
|
||||||
margin_top="8px",
|
|
||||||
),
|
|
||||||
# 思考折叠面板
|
|
||||||
rx.vstack(
|
|
||||||
# 标题
|
|
||||||
rx.box(
|
|
||||||
rx.hstack(
|
|
||||||
rx.text(
|
rx.text(
|
||||||
rx.cond(is_thinking, "思考中", "思考完成"),
|
dialog.user_prompt,
|
||||||
font_size="var(--prismui-font-size-1)",
|
max_width="600px",
|
||||||
color="var(--prismui-color-3)",
|
padding="12px 16px",
|
||||||
|
background_color="var(--prismui-background-color-3)",
|
||||||
|
border_radius="var(--prismui-border-radius-3)",
|
||||||
|
line_height="1.5",
|
||||||
|
word_wrap="break-word",
|
||||||
|
word_break="break-all",
|
||||||
|
white_space="pre-line",
|
||||||
),
|
),
|
||||||
rx.icon(
|
rx.hstack(
|
||||||
"chevron-right",
|
# 再次发送
|
||||||
width="14px",
|
rx.box(
|
||||||
height="14px",
|
rx.icon(
|
||||||
color="var(--prismui-color-3)",
|
"rotate-ccw",
|
||||||
transform=rx.cond(is_expanded, "rotate(90deg)", "rotate(0deg)"),
|
width="14px",
|
||||||
transition="transform 0.18s ease-in-out",
|
height="14px",
|
||||||
),
|
color="var(--prismui-color-2)",
|
||||||
align_items="center",
|
),
|
||||||
gap="4px",
|
padding="4px",
|
||||||
margin_bottom="8px",
|
border_radius="var(--prismui-border-radius-1)",
|
||||||
line_height="22px",
|
cursor="pointer",
|
||||||
),
|
style={
|
||||||
# 点击事件,展开/折叠思考折叠面板
|
"_hover": {
|
||||||
on_click=lambda: ConversationState.toggle_collapse(dialog.id),
|
"background_color": "var(--prismui-background-color-2)",
|
||||||
cursor="pointer",
|
}
|
||||||
),
|
},
|
||||||
# 思考
|
|
||||||
rx.box(
|
|
||||||
rx.box(
|
|
||||||
rx.auto_scroll(
|
|
||||||
rx.foreach(
|
|
||||||
dialog.thoughts,
|
|
||||||
thought_showing,
|
|
||||||
),
|
),
|
||||||
width="100%",
|
width="100%",
|
||||||
margin_top="8px",
|
margin_top="8px",
|
||||||
align_self="start",
|
|
||||||
),
|
),
|
||||||
style={"overflow": "hidden", "min-height": "0"},
|
|
||||||
),
|
),
|
||||||
display="grid",
|
align_items="flex-start",
|
||||||
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
|
gap="4px",
|
||||||
width="100%",
|
width="100%",
|
||||||
|
margin_top="8px",
|
||||||
|
),
|
||||||
|
rx.fragment(),
|
||||||
|
),
|
||||||
|
# 思考折叠面板
|
||||||
|
rx.cond(
|
||||||
|
dialog.thoughts,
|
||||||
|
rx.vstack(
|
||||||
|
# 标题
|
||||||
|
rx.box(
|
||||||
|
rx.hstack(
|
||||||
|
rx.text(
|
||||||
|
rx.cond(is_thinking, "思考中", "思考完成"),
|
||||||
|
font_size="var(--prismui-font-size-1)",
|
||||||
|
color="var(--prismui-color-3)",
|
||||||
|
),
|
||||||
|
rx.icon(
|
||||||
|
"chevron-right",
|
||||||
|
width="14px",
|
||||||
|
height="14px",
|
||||||
|
color="var(--prismui-color-3)",
|
||||||
|
transform=rx.cond(
|
||||||
|
is_expanded, "rotate(90deg)", "rotate(0deg)"
|
||||||
|
),
|
||||||
|
transition="transform 0.18s ease-in-out",
|
||||||
|
),
|
||||||
|
align_items="center",
|
||||||
|
gap="4px",
|
||||||
|
margin_bottom="8px",
|
||||||
|
line_height="22px",
|
||||||
|
),
|
||||||
|
# 点击事件,展开/折叠思考折叠面板
|
||||||
|
on_click=lambda: ConversationState.toggle_collapse(dialog.id),
|
||||||
|
cursor="pointer",
|
||||||
|
),
|
||||||
|
# 思考
|
||||||
|
rx.box(
|
||||||
|
rx.box(
|
||||||
|
rx.auto_scroll(
|
||||||
|
rx.foreach(
|
||||||
|
dialog.thoughts,
|
||||||
|
thought_showing,
|
||||||
|
),
|
||||||
|
width="100%",
|
||||||
|
align_self="start",
|
||||||
|
),
|
||||||
|
style={"overflow": "hidden", "min-height": "0"},
|
||||||
|
),
|
||||||
|
display="grid",
|
||||||
|
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
|
||||||
|
width="100%",
|
||||||
|
opacity=rx.cond(is_expanded, "1", "0"),
|
||||||
|
overflow="hidden",
|
||||||
|
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
|
||||||
|
),
|
||||||
margin_bottom="8px",
|
margin_bottom="8px",
|
||||||
opacity=rx.cond(is_expanded, "1", "0"),
|
),
|
||||||
overflow="hidden",
|
# 若思考列表为空且正在运行则显示加载动效
|
||||||
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
|
rx.cond(
|
||||||
|
ConversationState.running_status,
|
||||||
|
rx.box(
|
||||||
|
rx.box(
|
||||||
|
class_name="loadin",
|
||||||
|
flex_shrink="0",
|
||||||
|
),
|
||||||
|
display="flex",
|
||||||
|
justify_content="center",
|
||||||
|
align_items="center",
|
||||||
|
width="32px",
|
||||||
|
height="18px",
|
||||||
|
margin_bottom="8px",
|
||||||
|
overflow="hidden",
|
||||||
|
),
|
||||||
|
rx.fragment(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# 结果输出
|
# 结果输出
|
||||||
|
|
@ -643,58 +670,34 @@ def conversation_creating() -> rx.Component:
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
rx.spacer(),
|
rx.spacer(),
|
||||||
rx.dialog.root(
|
rx.box(
|
||||||
rx.dialog.trigger(
|
rx.tooltip(
|
||||||
rx.box(
|
rx.icon(
|
||||||
rx.tooltip(
|
"plus",
|
||||||
rx.icon(
|
on_click=ConversationState.create_conversation,
|
||||||
"plus",
|
width="14px",
|
||||||
width="14px",
|
height="14px",
|
||||||
height="14px",
|
style={
|
||||||
style={
|
"_hover": {
|
||||||
"_hover": {
|
"color": "var(--prismui-color-5)",
|
||||||
"color": "var(--prismui-color-5)",
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
content="新建会话",
|
|
||||||
side="top",
|
|
||||||
side_offset=9,
|
|
||||||
background_color="var(--prismui-background-color-1)",
|
|
||||||
box_shadow="var(--prismui-box-shadow-3)",
|
|
||||||
),
|
|
||||||
display="flex",
|
|
||||||
justify_content="center",
|
|
||||||
align_items="center",
|
|
||||||
width="24px",
|
|
||||||
height="24px",
|
|
||||||
background_color="var(--prismui-background-color-1)",
|
|
||||||
box_shadow="var(--prismui-box-shadow-8)",
|
|
||||||
border_radius="var(--prismui-border-radius-9)",
|
|
||||||
cursor="pointer",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
rx.dialog.content(
|
|
||||||
rx.form(
|
|
||||||
rx.hstack(
|
|
||||||
rx.input(
|
|
||||||
name="description",
|
|
||||||
placeholder="请输入会话描述",
|
|
||||||
flex="auto",
|
|
||||||
min_width="20ch",
|
|
||||||
),
|
|
||||||
rx.button("新建"),
|
|
||||||
spacing="2",
|
|
||||||
wrap="wrap",
|
|
||||||
width="100%",
|
|
||||||
),
|
|
||||||
# 提交事件:新建对话
|
|
||||||
on_submit=ConversationState.create_conversation,
|
|
||||||
),
|
),
|
||||||
background_color=rx.color("mauve", 1),
|
content="创建会话",
|
||||||
|
side="top",
|
||||||
|
side_offset=9,
|
||||||
|
background_color="var(--prismui-background-color-1)",
|
||||||
|
box_shadow="var(--prismui-box-shadow-3)",
|
||||||
),
|
),
|
||||||
open=ConversationState.is_conversation_creating,
|
display="flex",
|
||||||
on_open_change=ConversationState.toggle_conversation_creating,
|
justify_content="center",
|
||||||
|
align_items="center",
|
||||||
|
width="24px",
|
||||||
|
height="24px",
|
||||||
|
background_color="var(--prismui-background-color-1)",
|
||||||
|
box_shadow="var(--prismui-box-shadow-8)",
|
||||||
|
border_radius="var(--prismui-border-radius-9)",
|
||||||
|
cursor="pointer",
|
||||||
),
|
),
|
||||||
display="flex",
|
display="flex",
|
||||||
justify_content="flex-end",
|
justify_content="flex-end",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from asyncio import sleep
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
import re
|
import re
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from aiosmtplib import SMTP, SMTPException
|
from aiosmtplib import SMTP, SMTPException
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
@ -50,6 +50,9 @@ class AuthState(rx.State):
|
||||||
# 激活的导航按钮
|
# 激活的导航按钮
|
||||||
activated_nav_button: str = "conversation"
|
activated_nav_button: str = "conversation"
|
||||||
|
|
||||||
|
# 数据库状态
|
||||||
|
_db_state: Optional[DatabaseState] = None
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def reset_login_error_message(self) -> None:
|
def reset_login_error_message(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -91,6 +94,7 @@ class AuthState(rx.State):
|
||||||
):
|
):
|
||||||
self.login_error_message = "邮箱格式不正确,请重新输入"
|
self.login_error_message = "邮箱格式不正确,请重新输入"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.login_error_message = ""
|
self.login_error_message = ""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -123,10 +127,12 @@ class AuthState(rx.State):
|
||||||
发送验证码(后台任务)
|
发送验证码(后台任务)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
async with self:
|
async with self:
|
||||||
# 创建验证码记录并获取验证码
|
# 创建验证码记录并获取验证码
|
||||||
database_state = await self.get_state(DatabaseState)
|
captcha = await self._db_state.create_captcha_record(email=self.email)
|
||||||
captcha = await database_state.create_captcha_record(email=self.email)
|
|
||||||
|
|
||||||
# 构建验证码邮件
|
# 构建验证码邮件
|
||||||
message = MIMEText(
|
message = MIMEText(
|
||||||
|
|
@ -166,6 +172,9 @@ class AuthState(rx.State):
|
||||||
|
|
||||||
self.is_captcha_sent = True
|
self.is_captcha_sent = True
|
||||||
|
|
||||||
|
# 获取数据库状态
|
||||||
|
self._db_state = await self.get_state(DatabaseState)
|
||||||
|
|
||||||
# 将倒计时和发送验证码事件添加至后台任务队列
|
# 将倒计时和发送验证码事件添加至后台任务队列
|
||||||
yield type(self).countdown()
|
yield type(self).countdown()
|
||||||
yield type(self).send_captcha_background()
|
yield type(self).send_captcha_background()
|
||||||
|
|
@ -221,19 +230,20 @@ class AuthState(rx.State):
|
||||||
self.login_error_message = "请先阅读并同意协议和政策"
|
self.login_error_message = "请先阅读并同意协议和政策"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
self.is_logging_in = True
|
self.is_logging_in = True
|
||||||
|
|
||||||
# 获取数据库状态
|
|
||||||
database_state = await self.get_state(DatabaseState)
|
|
||||||
# 核验验证码
|
# 核验验证码
|
||||||
if not await database_state.verify_captcha(
|
if not await self._db_state.verify_captcha(
|
||||||
email=self.email, captcha=self.captcha
|
email=self.email, captcha=self.captcha
|
||||||
):
|
):
|
||||||
self.is_logging_in = False
|
self.is_logging_in = False
|
||||||
self.login_error_message = "验证码错误"
|
self.login_error_message = "验证码错误"
|
||||||
return
|
return
|
||||||
# 创建用户记录
|
# 创建用户记录
|
||||||
user_id = await database_state.create_user_record(email=self.email)
|
user_id = await self._db_state.create_user_record(email=self.email)
|
||||||
|
|
||||||
# 恢复当前用户会话状态
|
# 恢复当前用户会话状态
|
||||||
conversation_state = await self.get_state(ConversationState)
|
conversation_state = await self.get_state(ConversationState)
|
||||||
|
|
@ -271,7 +281,6 @@ class AuthState(rx.State):
|
||||||
conversation_state.conversations = {}
|
conversation_state.conversations = {}
|
||||||
conversation_state.conversation_id = ""
|
conversation_state.conversation_id = ""
|
||||||
conversation_state.is_conversation_history_shown = False
|
conversation_state.is_conversation_history_shown = False
|
||||||
conversation_state.is_conversation_creating = False
|
|
||||||
|
|
||||||
self.user_id = ""
|
self.user_id = ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
会话状态
|
会话状态
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, AsyncGenerator, Dict, List
|
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||||
from pydantic_ai import Agent, ThinkingPartDelta
|
from pydantic_ai import Agent, ThinkingPartDelta, ModelMessage
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
FunctionToolCallEvent,
|
FunctionToolCallEvent,
|
||||||
FunctionToolResultEvent,
|
FunctionToolResultEvent,
|
||||||
|
|
@ -18,48 +18,19 @@ from pydantic_ai.messages import (
|
||||||
ToolCallPart,
|
ToolCallPart,
|
||||||
ToolSearchCallPart,
|
ToolSearchCallPart,
|
||||||
)
|
)
|
||||||
from pydantic_ai.models.openai import OpenAIChatModel
|
|
||||||
from pydantic_ai.providers.openai import OpenAIProvider
|
|
||||||
from pydantic_ai.run import AgentRunResultEvent
|
from pydantic_ai.run import AgentRunResultEvent
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
|
|
||||||
from application.states.database import DatabaseState
|
from application.states.database import DatabaseState
|
||||||
from application.domain_models import (
|
from application.domain_models import (
|
||||||
Conversation,
|
Conversation,
|
||||||
Dialog,
|
Dialog,
|
||||||
Thought,
|
Thought,
|
||||||
|
TaskType,
|
||||||
)
|
)
|
||||||
|
from application.agents import chat_agent
|
||||||
instructions: str = """
|
|
||||||
# 角色
|
|
||||||
专业友好AI助手,结构化解答各类问题。
|
|
||||||
|
|
||||||
# 输出硬性规则
|
|
||||||
1. 全文强制标准Markdown,禁止纯文本;不要额外说明排版格式,直接输出内容;
|
|
||||||
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
|
|
||||||
3. 代码块用 ```语言名``` 包裹;
|
|
||||||
4. 重点内容标注 **粗体**/*斜体*;
|
|
||||||
5. 思考、工具日志仅输出文本,适配前端折叠面板,禁止输出HTML标签;
|
|
||||||
6. 内容分点拆分,排版整洁适配前端Markdown渲染。
|
|
||||||
|
|
||||||
# 行文要求
|
|
||||||
语言通俗,逻辑完整简洁,无多余废话。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 实例化智能体(因无法序列化故剥离出状态管理)
|
|
||||||
agent: Agent = Agent(
|
|
||||||
model=OpenAIChatModel(
|
|
||||||
model_name="deepseek-v4-flash",
|
|
||||||
provider=OpenAIProvider(
|
|
||||||
base_url="https://tokenhub.tencentmaas.com/v1",
|
|
||||||
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
instructions=instructions,
|
|
||||||
capabilities=None,
|
|
||||||
output_type=str,
|
|
||||||
retries=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationState(rx.State):
|
class ConversationState(rx.State):
|
||||||
|
|
@ -79,12 +50,14 @@ class ConversationState(rx.State):
|
||||||
# 显示更多的会话唯一标识
|
# 显示更多的会话唯一标识
|
||||||
shown_more_conversation_id: str = ""
|
shown_more_conversation_id: str = ""
|
||||||
|
|
||||||
# 会话创建状态,True表示正在创建,False表示未正在创建
|
# 任务类型
|
||||||
is_conversation_creating: bool = False
|
task_type: TaskType = TaskType.CHAT
|
||||||
|
|
||||||
# 用户提示词
|
# 用户提示词
|
||||||
user_prompt: str = ""
|
user_prompt: str = ""
|
||||||
|
|
||||||
|
# 数据库状态(私有变量,不予序列化)
|
||||||
|
_db_state: Optional[DatabaseState] = None
|
||||||
|
|
||||||
async def resume(self, user_id: str) -> None:
|
async def resume(self, user_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
恢复当前用户会话状态
|
恢复当前用户会话状态
|
||||||
|
|
@ -95,18 +68,19 @@ class ConversationState(rx.State):
|
||||||
if not self.user_id:
|
if not self.user_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
database_state = await self.get_state(DatabaseState)
|
self._db_state = await self.get_state(DatabaseState)
|
||||||
|
|
||||||
# 获取会话字典
|
# 获取会话字典
|
||||||
self.conversations = await database_state.get_conversations(
|
self.conversations = await self._db_state.get_conversations(
|
||||||
user_id=self.user_id
|
user_id=self.user_id
|
||||||
)
|
)
|
||||||
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
|
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
|
||||||
if not self.conversations:
|
if not self.conversations:
|
||||||
self.conversations.update(
|
self.conversations.update(
|
||||||
await database_state.create_conversations_record(user_id=self.user_id)
|
await self._db_state.create_conversations_record(user_id=self.user_id)
|
||||||
)
|
)
|
||||||
# 将最后一个会话作为当前会话并更新会话唯一标识
|
# 将最后一个会话作为当前会话并更新会话唯一标识
|
||||||
self.conversation_id = next(reversed(self.conversations))
|
self.conversation_id = next(reversed(self.conversations.keys()))
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def toggle_conversation_history_shown(self) -> None:
|
def toggle_conversation_history_shown(self) -> None:
|
||||||
|
|
@ -133,9 +107,11 @@ class ConversationState(rx.State):
|
||||||
:param conversation_id: 需删除的会话唯一标识
|
:param conversation_id: 需删除的会话唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
# 先设置会话记录为已删除再在会话字典中删除会话实例
|
# 先设置会话记录为已删除再在会话字典中删除会话实例
|
||||||
database_state = await self.get_state(DatabaseState)
|
await self._db_state.set_conversations_record_deleted(
|
||||||
await database_state.set_conversations_record_deleted(
|
|
||||||
conversation_id=conversation_id
|
conversation_id=conversation_id
|
||||||
)
|
)
|
||||||
del self.conversations[conversation_id]
|
del self.conversations[conversation_id]
|
||||||
|
|
@ -143,12 +119,12 @@ class ConversationState(rx.State):
|
||||||
# 删除后,若会话字典为空则先创建会话记录再添加会话实例
|
# 删除后,若会话字典为空则先创建会话记录再添加会话实例
|
||||||
if not self.conversations:
|
if not self.conversations:
|
||||||
self.conversations.update(
|
self.conversations.update(
|
||||||
await database_state.create_conversations_record(user_id=self.user_id)
|
await self._db_state.create_conversations_record(user_id=self.user_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识
|
# 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识
|
||||||
if self.conversation_id not in self.conversations:
|
if self.conversation_id not in self.conversations:
|
||||||
self.conversation_id = next(reversed(self.conversations))
|
self.conversation_id = next(reversed(self.conversations.keys()))
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def switch_conversation(self, conversation_id: str) -> None:
|
def switch_conversation(self, conversation_id: str) -> None:
|
||||||
|
|
@ -160,34 +136,22 @@ class ConversationState(rx.State):
|
||||||
self.conversation_id = conversation_id
|
self.conversation_id = conversation_id
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
async def create_conversation(self, form_data: Dict[str, Any]) -> None:
|
async def create_conversation(self) -> None:
|
||||||
"""
|
"""
|
||||||
创建会话
|
创建会话
|
||||||
:param form_data: 表单数据
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 解析会话描述
|
|
||||||
description = form_data["description"].strip() or "新会话"
|
|
||||||
|
|
||||||
# 创建会话记录再添加会话实例
|
# 创建会话记录再添加会话实例
|
||||||
database_state = await self.get_state(DatabaseState)
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
self.conversations.update(
|
self.conversations.update(
|
||||||
await database_state.create_conversations_record(
|
await self._db_state.create_conversations_record(
|
||||||
user_id=self.user_id, description=description
|
user_id=self.user_id, description="新会话"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# 将最后一个会话作为当前会话并更新会话唯一标识
|
# 将最后一个会话作为当前会话并更新会话唯一标识
|
||||||
self.conversation_id = next(reversed(self.conversations))
|
self.conversation_id = next(reversed(self.conversations.keys()))
|
||||||
|
|
||||||
# 会话创建状态设置为未正在创建
|
|
||||||
self.is_conversation_creating = False
|
|
||||||
|
|
||||||
@rx.event
|
|
||||||
def toggle_conversation_creating(self) -> None:
|
|
||||||
"""
|
|
||||||
切换会话创建状态
|
|
||||||
"""
|
|
||||||
self.is_conversation_creating = not self.is_conversation_creating
|
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def set_user_prompt(self, user_prompt: str) -> None:
|
def set_user_prompt(self, user_prompt: str) -> None:
|
||||||
|
|
@ -234,46 +198,30 @@ class ConversationState(rx.State):
|
||||||
return {}
|
return {}
|
||||||
return conversation.dialogs
|
return conversation.dialogs
|
||||||
|
|
||||||
@rx.event
|
async def run_stream_events(
|
||||||
async def handle_user_prompt(self) -> AsyncGenerator[None]:
|
self,
|
||||||
|
conversation: Conversation,
|
||||||
|
dialog: Dialog,
|
||||||
|
agent: Agent,
|
||||||
|
message_history: Optional[List[ModelMessage]] = None,
|
||||||
|
) -> AsyncGenerator[None]:
|
||||||
"""
|
"""
|
||||||
处理用户提示词
|
运行并处理流式事件
|
||||||
|
:param agent: 代理
|
||||||
|
:param conversation: 当前会话
|
||||||
|
:param dialog: 当前对话
|
||||||
|
:param message_history: 消息历史列表
|
||||||
:return: AsyncGenerator[None]
|
:return: AsyncGenerator[None]
|
||||||
"""
|
"""
|
||||||
if not self.user_prompt:
|
if not self._db_state:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 清空前端用户提示词
|
|
||||||
user_prompt = self.user_prompt
|
|
||||||
self.user_prompt = ""
|
|
||||||
|
|
||||||
# 当前会话
|
|
||||||
conversation = self.conversations[self.conversation_id]
|
|
||||||
# 将运行状态设置为正在运行
|
|
||||||
conversation.is_running = True
|
|
||||||
|
|
||||||
database_state = await self.get_state(DatabaseState)
|
|
||||||
|
|
||||||
# 创建对话记录再添加对话实例
|
|
||||||
conversation.dialogs.update(
|
|
||||||
await database_state.create_dialog_record(
|
|
||||||
conversation_id=self.conversation_id, user_prompt=user_prompt
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# 强制更新会话并推送前端
|
|
||||||
self.conversations[self.conversation_id] = conversation
|
|
||||||
yield
|
|
||||||
|
|
||||||
# 将最后一个对话作为当前对话
|
|
||||||
dialog = next(reversed(conversation.dialogs.values()))
|
|
||||||
# 初始化工具调用唯一标识和片段索引映射字典
|
# 初始化工具调用唯一标识和片段索引映射字典
|
||||||
tool_call_ids: Dict[str, int] = {}
|
tool_call_ids: Dict[str, int] = {}
|
||||||
async with agent.run_stream_events(
|
async with agent.run_stream_events(
|
||||||
conversation_id=self.conversation_id,
|
conversation_id=self.conversation_id,
|
||||||
user_prompt=user_prompt,
|
user_prompt=dialog.user_prompt,
|
||||||
message_history=await database_state.get_message_history(
|
message_history=message_history,
|
||||||
conversation_id=self.conversation_id
|
|
||||||
),
|
|
||||||
) as events:
|
) as events:
|
||||||
async for event in events:
|
async for event in events:
|
||||||
match event:
|
match event:
|
||||||
|
|
@ -397,25 +345,91 @@ class ConversationState(rx.State):
|
||||||
|
|
||||||
# ========== 智能体运行结果事件 ==========
|
# ========== 智能体运行结果事件 ==========
|
||||||
case AgentRunResultEvent(result=result):
|
case AgentRunResultEvent(result=result):
|
||||||
|
# 获取数据库状态
|
||||||
# 补全对话记录
|
# 补全对话记录
|
||||||
await database_state.complete_dialog_record(
|
await self._db_state.complete_dialog_record(
|
||||||
id=dialog.id,
|
id=dialog.id,
|
||||||
thoughts=dialog.thoughts,
|
thoughts=dialog.thoughts,
|
||||||
result_output=dialog.result_output,
|
result_output=dialog.result_output,
|
||||||
)
|
)
|
||||||
# 创建结果记录
|
# 创建结果记录
|
||||||
await database_state.create_result_record(
|
await self._db_state.create_result_record(
|
||||||
conversation_id=self.conversation_id,
|
conversation_id=self.conversation_id,
|
||||||
dialog_id=dialog.id,
|
dialog_id=dialog.id,
|
||||||
new_messages=result.new_messages(),
|
new_messages=result.new_messages(),
|
||||||
)
|
)
|
||||||
# 将当前会话的运行状态设置为运行完成
|
# 将正在运行设置为否
|
||||||
conversation.is_running = False
|
conversation.is_running = False
|
||||||
|
|
||||||
# 强制更新会话并推送前端
|
# 强制更新会话并推送前端
|
||||||
self.conversations[self.conversation_id] = conversation
|
self.conversations[self.conversation_id] = conversation
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
async def handle_user_prompt(self) -> AsyncGenerator[None]:
|
||||||
|
"""
|
||||||
|
处理用户提示词
|
||||||
|
:return: AsyncGenerator[None]
|
||||||
|
"""
|
||||||
|
if not self.user_prompt:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 当前会话
|
||||||
|
conversation = self.conversations[self.conversation_id]
|
||||||
|
# 将正在运行设置为是
|
||||||
|
conversation.is_running = True
|
||||||
|
# 创建对话记录再添加对话实例
|
||||||
|
conversation.dialogs.update(
|
||||||
|
await self._db_state.create_dialog_record(
|
||||||
|
conversation_id=self.conversation_id, user_prompt=self.user_prompt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 将最后一个对话作为当前对话
|
||||||
|
dialog = next(reversed(conversation.dialogs.values()))
|
||||||
|
# 清空前端用户提示词
|
||||||
|
self.user_prompt = ""
|
||||||
|
# 强制更新会话并推送前端
|
||||||
|
self.conversations[self.conversation_id] = conversation
|
||||||
|
yield
|
||||||
|
|
||||||
|
match self.task_type:
|
||||||
|
# 开放式对话
|
||||||
|
case TaskType.CHAT:
|
||||||
|
async for _ in self.run_stream_events(
|
||||||
|
conversation=conversation,
|
||||||
|
dialog=dialog,
|
||||||
|
agent=chat_agent,
|
||||||
|
message_history=await self._db_state.get_message_history(
|
||||||
|
conversation_id=self.conversation_id
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
case TaskType.GENERATING_PRD:
|
||||||
|
...
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
async def generate_prd(self) -> AsyncGenerator[None]:
|
||||||
|
"""
|
||||||
|
生成产品需求文档
|
||||||
|
"""
|
||||||
|
if not self._db_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 当前会话
|
||||||
|
conversation = self.conversations[self.conversation_id]
|
||||||
|
|
||||||
|
# 创建对话记录再添加对话实例
|
||||||
|
conversation.dialogs.update(
|
||||||
|
await self._db_state.create_dialog_record(
|
||||||
|
conversation_id=self.conversation_id, result_output="请输入产品需求"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def toggle_collapse(self, dialog_id: str) -> None:
|
def toggle_collapse(self, dialog_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -298,23 +298,29 @@ class DatabaseState(rx.State):
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def create_dialog_record(
|
async def create_dialog_record(
|
||||||
self, conversation_id: str, user_prompt: str
|
self, conversation_id: str, user_prompt: str = "", result_output: str = ""
|
||||||
) -> Dict[str, Dialog]:
|
) -> Dict[str, Dialog]:
|
||||||
"""
|
"""
|
||||||
创建对话记录
|
创建对话记录
|
||||||
:param conversation_id: 会话唯一标识
|
:param conversation_id: 会话唯一标识
|
||||||
:param user_prompt: 用户提示词
|
:param user_prompt: 用户提示词
|
||||||
|
:param result_output: 结果输出
|
||||||
:return: 创建对话记录的唯一标识
|
:return: 创建对话记录的唯一标识
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
record = DialogRecord(
|
record = DialogRecord(
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
user_prompt=user_prompt,
|
user_prompt=user_prompt,
|
||||||
|
result_output=result_output,
|
||||||
)
|
)
|
||||||
session.add(record)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(record)
|
await session.refresh(record)
|
||||||
return {record.id: Dialog(id=record.id, user_prompt=user_prompt)}
|
return {
|
||||||
|
record.id: Dialog(
|
||||||
|
id=record.id, user_prompt=user_prompt, result_output=result_output
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async def complete_dialog_record(
|
async def complete_dialog_record(
|
||||||
self,
|
self,
|
||||||
|
|
@ -366,7 +372,7 @@ class DatabaseState(rx.State):
|
||||||
|
|
||||||
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
|
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
|
||||||
"""
|
"""
|
||||||
获取消息历史
|
获取消息历史列表
|
||||||
:param conversation_id: 会话唯一标识
|
:param conversation_id: 会话唯一标识
|
||||||
:return: 消息历史
|
:return: 消息历史
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,29 @@ pre {
|
||||||
|
|
||||||
pre, pre code {
|
pre, pre code {
|
||||||
font-family: Consolas, "Microsoft YaHei", sans-serif !important;
|
font-family: Consolas, "Microsoft YaHei", sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadin {
|
||||||
|
width: 6px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: loadin 0.9s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes loadin {
|
||||||
|
0% {
|
||||||
|
box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9);
|
||||||
|
background: var(--prismui-color-5);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9);
|
||||||
|
background: var(--prismui-color-9);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
box-shadow: 12px 0 var(--prismui-color-9), -12px 0 var(--prismui-color-5);
|
||||||
|
background: var(--prismui-color-9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 12px 0 var(--prismui-color-9), -12px 0 var(--prismui-color-5);
|
||||||
|
background: var(--prismui-color-5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
|
@ -5,7 +5,6 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "reflex",
|
"name": "reflex",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-form": "0.1.8",
|
|
||||||
"@radix-ui/themes": "3.3.0",
|
"@radix-ui/themes": "3.3.0",
|
||||||
"@react-router/node": "7.15.0",
|
"@react-router/node": "7.15.0",
|
||||||
"isbot": "5.1.40",
|
"isbot": "5.1.40",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
"export": "react-router build"
|
"export": "react-router build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-form": "0.1.8",
|
|
||||||
"@radix-ui/themes": "3.3.0",
|
"@radix-ui/themes": "3.3.0",
|
||||||
"@react-router/node": "7.15.0",
|
"@react-router/node": "7.15.0",
|
||||||
"isbot": "5.1.40",
|
"isbot": "5.1.40",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue