临时保存

This commit is contained in:
Wenjie Zhang 2025-04-02 00:00:04 +08:00
parent 04ee276331
commit 9451b01ae9
9 changed files with 373 additions and 105 deletions

View File

@ -1,8 +1,15 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from langchain_community.tools.tavily_search import TavilySearchResults
from src.agents.registry import Configuration
from src.agents.tools_factory import multiply, add, subtract, divide
def get_default_tools():
return ["TavilySearchResults", "multiply", "add", "subtract", "divide"]
@dataclass(kw_only=True)
class ChatbotConfiguration(Configuration):
@ -24,3 +31,32 @@ class ChatbotConfiguration(Configuration):
},
)
tools: list = field(
default_factory=get_default_tools,
metadata={
"description": "The tools to use for the agent's interactions. "
"Should be in the form: provider/model-name."
},
)
temperature: float = field(
default=0.7,
metadata={
"description": "控制模型生成结果的随机性,值越大随机性越高,建议范围 0.0-1.0"
},
)
use_tools: bool = field(
default=True,
metadata={
"description": "是否启用工具调用功能"
},
)
max_iterations: int = field(
default=10,
metadata={
"description": "智能体最大执行步数,防止无限循环"
},
)

View File

@ -6,14 +6,13 @@ from datetime import datetime
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.checkpoint.memory import MemorySaver # 实际上没有起作用
from src.agents.registry import State, BaseAgent
from src.agents.utils import load_chat_model
from src.agents.tools_factory import multiply, add, subtract, divide
from src.agents.chatbot.configuration import ChatbotConfiguration
from src.agents.tools_factory import _TOOLS_REGISTRY
class ChatbotAgent(BaseAgent):
name = "chatbot"
@ -27,14 +26,15 @@ class ChatbotAgent(BaseAgent):
def _get_tools(self, config_schema: RunnableConfig):
"""根据配置获取工具"""
tools = [multiply, add, subtract, divide, TavilySearchResults(max_results=10)]
return tools
default_tools_names = config_schema.get("tools", [])
default_tools = [_TOOLS_REGISTRY[tool] for tool in default_tools_names]
return default_tools
def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]:
"""调用 llm 模型"""
config_schema = config or {}
conf = self.config_schema.from_runnable_config(config_schema)
model = load_chat_model(conf.model)
model = load_chat_model(conf.model, temperature=conf.temperature)
model_with_tools = model.bind_tools(self._get_tools(config_schema))
res = model_with_tools.invoke(

View File

@ -42,7 +42,9 @@ class Configuration(SimpleConfig):
@classmethod
def to_dict(cls):
return {f.name: getattr(cls, f.name) for f in fields(cls) if f.init}
# 创建一个实例来处理 default_factory
instance = cls()
return {f.name: getattr(instance, f.name) for f in fields(cls) if f.init}
@ -84,12 +86,13 @@ class BaseAgent():
def stream_messages(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs):
graph = self.get_graph(config_schema=config_schema, **kwargs)
conf = self.config_schema.from_runnable_config(config_schema)
for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config_schema):
msg_type = msg.type
return_keys =conf.return_keys
if not return_keys or msg_type in return_keys:
yield msg, metadata
for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config_schema):
# msg_type = msg.type
# return_keys = conf.get("return_keys", [])
# if not return_keys or msg_type in return_keys:
# yield msg, metadata
yield msg, metadata
@abstractmethod
def get_graph(self, **kwargs) -> CompiledStateGraph:

View File

@ -6,9 +6,7 @@ from typing import Any, Callable, Optional, Type, Union
from pydantic import BaseModel, Field
from langchain_core.tools import tool, BaseTool
_TOOLS_REGISTRY = {}
from langchain_community.tools.tavily_search import TavilySearchResults
# refs https://github.com/chatchat-space/LangGraph-Chatchat chatchat-server/chatchat/server/agent/tools_factory/tools_registry.py
def regist_tool(
@ -115,3 +113,12 @@ def subtract(first_int: int, second_int: int) -> int:
def divide(first_int: int, second_int: int) -> int:
"""Divide two integers."""
return first_int / second_int
_TOOLS_REGISTRY = {
"multiply": multiply,
"add": add,
"subtract": subtract,
"divide": divide,
"TavilySearchResults": TavilySearchResults(max_results=10),
}

View File

@ -7,14 +7,23 @@ from langchain_core.messages import AIMessageChunk, ToolMessage
def load_chat_model(fully_specified_name: str) -> BaseChatModel:
def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel:
"""Load a chat model from a fully specified name.
Args:
fully_specified_name (str): String in the format 'provider/model'.
**kwargs: Additional parameters to pass to the model.
"""
provider, model = fully_specified_name.split("/", maxsplit=1)
return select_model(model_name=model, model_provider=provider).chat_open_ai
model_instance = select_model(model_name=model, model_provider=provider)
# 配置额外参数如temperature
if kwargs and hasattr(model_instance, 'chat_open_ai'):
for key, value in kwargs.items():
if value is not None:
setattr(model_instance.chat_open_ai, key, value)
return model_instance.chat_open_ai
def agent_cli(agent: BaseAgent, config: RunnableConfig = None):

View File

@ -11,7 +11,8 @@ class OpenAIBase():
self.model_name = model_name
self.chat_open_ai = ChatOpenAI(model=model_name,
api_key=api_key,
base_url=base_url)
base_url=base_url,
temperature=0.7)
def predict(self, message, stream=False):
if isinstance(message, str):

View File

@ -130,17 +130,21 @@ async def get_agent():
@chat.post("/agent/{agent_name}")
def chat_agent(agent_name: str,
query: str = Body(...),
meta: dict = Body({}),
history: list = Body(...),
thread_id: str | None = Body(None)):
meta["server_model_name"] = agent_name
config: dict = Body({})):
# 将meta和thread_id整合到config中
def make_chunk(content=None, **kwargs):
chat_metadata = {
"agent_name": agent_name,
"thread_id": config.get("thread_id"),
}
if update_metadata := kwargs.get("chat_metadata"):
chat_metadata.update(update_metadata)
return json.dumps({
"response": content,
"model_name": agent_name,
"meta": meta,
"chat_metadata": chat_metadata,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n"
@ -150,22 +154,27 @@ def chat_agent(agent_name: str,
logger.error(f"Error getting agent {agent_name}: {e}")
return StreamingResponse(make_chunk(message=f"Error getting agent {agent_name}: {e}", status="error"), media_type='application/json')
# 从config中获取history_round
history_round = config.get("history_round")
history_manager = HistoryManager(history)
messages = history_manager.get_history_with_msg(query, max_rounds=meta.get('history_round'))
history_manager.add_user(query) # 注意这里使用原始查询
messages = history_manager.get_history_with_msg(query, max_rounds=history_round)
history_manager.add_user(query)
# 如果没有thread_id则生成一个
if "thread_id" not in config or not config["thread_id"]:
config["thread_id"] = str(uuid.uuid4())
# 构造运行时配置
runnable_config = {
"configurable": {
"thread_id": thread_id or str(uuid.uuid4()),
"return_keys": []
**config
}
}
def stream_messages():
content = ""
yield make_chunk(status="waiting")
for msg, metadata in agent.stream_messages(messages, runnable_config):
# logger.debug(f">>>>> msg: {msg.model_dump()}, >>>>>>> {metadata=}")
yield make_chunk(status="init")
for msg, metadata in agent.stream_messages(messages, config_schema=runnable_config):
if isinstance(msg, AIMessageChunk) and msg.content != "<tool_call>":
content += msg.content
yield make_chunk(content=msg.content,

View File

@ -8,28 +8,16 @@
<PlusCircleOutlined /> <span class="text">新对话</span>
</div>
</div>
<div class="header__center">
<slot name="header-center"></slot>
</div>
<div class="header__right">
<div v-if="!props.agentId" class="current-agent nav-btn">
<a-dropdown>
<div class="current-agent nav-btn">
<RobotOutlined />&nbsp;
<span v-if="currentAgent">{{ currentAgent.name }}</span>
<span v-else>请选择智能体</span>
</div>
<template #overlay>
<a-menu @click="({key}) => selectAgent(key)">
<a-menu-item v-for="(agent, name) in agents" :key="name">
<RobotOutlined /> {{ agent.name }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-else class="current-agent nav-btn">
<div class="current-agent nav-btn" @click="sayHi">
<RobotOutlined />&nbsp;
<span v-if="currentAgent">{{ currentAgent.name }}</span>
<span v-else>加载中...</span>
</div>
<slot name="header-right"></slot>
</div>
</div>
@ -38,7 +26,7 @@
<p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p>
</div>
<div class="chat-box" ref="messagesContainer" :class="{ 'is-debug': options.debug_mode }">
<div class="chat-box" ref="messagesContainer" :class="{ 'is-debug': config.debug_mode }">
<MessageComponent
v-for="(message, index) in messages"
:message="message"
@ -46,7 +34,7 @@
:is-processing="isProcessing"
@retry="retryMessage(index)"
>
<div v-if="options.debug_mode" class="status-info">{{ message }}</div>
<div v-if="config.debug_mode" class="status-info">{{ message }}</div>
<!-- 工具调用 -->
<template #tool-calls>
@ -112,12 +100,12 @@
<script setup>
import { ref, reactive, onMounted, watch, nextTick, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
RobotOutlined, SendOutlined, LoadingOutlined,
ThunderboltOutlined, ReloadOutlined, CheckCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import MessageInputComponent from '@/components/MessageInputComponent.vue'
import MessageComponent from '@/components/MessageComponent.vue'
@ -126,23 +114,18 @@ const props = defineProps({
agentId: {
type: String,
default: null
},
config: {
type: Object,
default: () => ({})
}
});
// 使props
const route = useRoute();
const router = useRouter();
// ==================== ====================
// UI
const state = reactive({});
//
const options = reactive({
use_web: true,
debug_mode: false,
});
// DOM
const messagesContainer = ref(null);
@ -153,7 +136,6 @@ const currentAgent = ref(null); // 当前选中的智能体
const userInput = ref(''); //
const messages = ref([]); //
const isProcessing = ref(false); //
const threadId = ref(null); // 线ID
// ==================== ====================
@ -200,7 +182,6 @@ const resetStatusSteps = () => {
// 线
const resetThread = () => {
threadId.value = null;
messages.value = [];
resetStatusSteps();
saveState();
@ -274,7 +255,6 @@ const prepareMessageHistory = (msgs) => {
const selectAgent = (agentName) => {
currentAgent.value = agents.value[agentName];
messages.value = [];
threadId.value = null;
resetStatusSteps();
saveState();
};
@ -352,11 +332,10 @@ const sendMessageWithText = async (text) => {
//
const requestData = {
query: userMessage,
meta: {
use_web: options.use_web
},
history: history.slice(0, -1), //
thread_id: threadId.value
config: {
...props.config
}
};
//
@ -663,11 +642,6 @@ onMounted(async () => {
//
const handleMetadata = (data) => {
// 线ID
if (data.metadata?.thread_id && !threadId.value) {
threadId.value = data.metadata.thread_id;
}
// ID
if (data.metadata?.run_id && !currentRunId.value) {
currentRunId.value = data.metadata.run_id;
@ -707,16 +681,6 @@ const loadState = () => {
console.log("loadState with prefix:", storagePrefix);
//
const savedOptions = localStorage.getItem(`${storagePrefix}-options`);
if (savedOptions) {
try {
Object.assign(options, JSON.parse(savedOptions));
} catch (e) {
console.error('解析选项数据出错:', e);
}
}
//
const savedMessages = localStorage.getItem(`${storagePrefix}-messages`);
if (savedMessages) {
@ -737,8 +701,8 @@ const loadState = () => {
// 线ID
const savedThreadId = localStorage.getItem(`${storagePrefix}-thread-id`);
if (savedThreadId) {
threadId.value = savedThreadId;
console.log(`加载线程ID (${storagePrefix}):`, threadId.value);
currentRunId.value = savedThreadId;
console.log(`加载线程ID (${storagePrefix}):`, currentRunId.value);
}
} catch (error) {
console.error('从localStorage加载状态出错:', error);
@ -754,7 +718,7 @@ watch(() => props.agentId, async (newAgentId, oldAgentId) => {
if (newAgentId !== oldAgentId) {
//
messages.value = [];
threadId.value = null;
currentRunId.value = null;
resetStatusSteps();
//
@ -819,9 +783,6 @@ const saveState = () => {
localStorage.setItem(`${prefix}-current-agent`, currentAgent.value.name);
}
//
localStorage.setItem(`${prefix}-options`, JSON.stringify(options));
//
if (messages.value && messages.value.length > 0) {
console.log(`保存消息历史 (${prefix}):`, messages.value.length);
@ -831,9 +792,9 @@ const saveState = () => {
}
// 线ID
if (threadId.value) {
localStorage.setItem(`${prefix}-thread-id`, threadId.value);
console.log(`保存线程ID (${prefix}):`, threadId.value);
if (currentRunId.value) {
localStorage.setItem(`${prefix}-thread-id`, currentRunId.value);
console.log(`保存线程ID (${prefix}):`, currentRunId.value);
} else {
localStorage.removeItem(`${prefix}-thread-id`);
}
@ -842,8 +803,12 @@ const saveState = () => {
}
};
const sayHi = () => {
message.success(`Hi, I am ${currentAgent.value.name}, ${currentAgent.value.description}`);
}
//
watch([currentAgent, options, messages, threadId], () => {
watch([currentAgent, messages, currentRunId], () => {
try {
saveState();
} catch (error) {

View File

@ -1,5 +1,6 @@
<template>
<div class="agent-view">
<!-- 左侧智能体列表侧边栏 -->
<div class="sidebar" :class="{ 'is-open': state.isSidebarOpen }">
<h2 class="sidebar-title">
智能体列表
@ -37,21 +38,113 @@
</div>
</div>
</div>
<!-- 中间内容区域 -->
<div class="content">
<AgentChatComponent :agent-id="selectedAgentId">
<AgentChatComponent
:agent-id="selectedAgentId"
:config="agentConfig"
@open-config="toggleConfigSidebar(true)"
>
<template #header-left>
<div class="toggle-sidebar nav-btn" @click="toggleSidebar" v-if="!state.isSidebarOpen">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
</div>
</template>
<template #header-right>
<div class="toggle-sidebar nav-btn" @click="toggleConfigSidebar()">
<SettingOutlined class="iconfont icon-20" />
<span class="text">配置</span>
</div>
</template>
</AgentChatComponent>
</div>
<!-- 右侧配置侧边栏 -->
<div class="config-sidebar" :class="{ 'is-open': state.isConfigSidebarOpen }">
<h2 class="sidebar-title">
智能体配置
<div class="toggle-sidebar" @click="toggleConfigSidebar(false)">
<CloseOutlined class="iconfont icon-20" />
</div>
</h2>
<div v-if="selectedAgentId && configSchema" class="config-form">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
<!-- 系统提示词 -->
<div class="empty-config" v-if="state.isEmptyConfig">
<a-alert type="warning" message="该智能体没有配置项" show-icon/>
</div>
<a-form-item v-if="configSchema.system_prompt" label="系统提示词" name="system_prompt">
<a-textarea
v-model:value="agentConfig.system_prompt"
:rows="4"
placeholder="设置智能体的系统提示词"
/>
</a-form-item>
<!-- 模型选择 -->
<a-form-item v-if="configSchema.model" :label="`模型 (默认: ${configSchema.model})`" name="model">
<a-input
v-model:value="agentConfig.model"
placeholder="provider/model-name"
/>
</a-form-item>
<!-- 工具选择: 多选默认全选 -->
<a-form-item v-if="configSchema.tools" :label="`工具`" name="tools">
<a-select
v-model:value="agentConfig.tools"
placeholder="选择工具"
mode="multiple"
>
<a-select-option v-for="tool in configSchema.tools" :key="tool">
{{ tool }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 其他配置项可按需扩展 -->
<div v-if="Object.keys(additionalConfig).length > 0">
<a-divider>其他配置</a-divider>
<a-form-item
v-for="(value, key) in additionalConfig"
:key="key"
:label="key"
:name="key"
>
<a-input
v-model:value="additionalConfig[key]"
:placeholder="`设置${key}`"
/>
</a-form-item>
</div>
<!-- 保存和重置按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig">
<a-button type="primary" @click="saveConfig">保存配置</a-button>
<a-button @click="resetConfig">重置</a-button>
</div>
</a-form>
</div>
<div v-else class="no-agent-selected">
请先选择一个智能体
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, reactive, watch } from 'vue';
import { RobotOutlined, MenuFoldOutlined, MenuUnfoldOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { ref, onMounted, reactive, watch, computed, h } from 'vue';
import {
RobotOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
CloseOutlined,
SettingOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
//
@ -59,7 +152,80 @@ const agents = ref({});
const selectedAgentId = ref(null);
const state = reactive({
isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'),
isConfigSidebarOpen: false,
isEmptyConfig: computed(() => Object.keys(agents.value[selectedAgentId.value]?.config_schema || {}).length === 0),
});
const configSchema = computed(() => agents.value[selectedAgentId.value]?.config_schema || {});
//
const agentConfig = ref({
system_prompt: '',
model: '',
debug_mode: false,
});
//
const additionalConfig = ref({});
//
const loadAgentConfig = () => {
if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return;
const agent = agents.value[selectedAgentId.value];
const configSchema = agent.config_schema || {};
//
agentConfig.value = {
system_prompt: configSchema.system_prompt || '',
model: configSchema.model || '',
//
tools: configSchema.tools || [],
};
//
additionalConfig.value = {};
//
const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}');
//
if (savedConfig) {
Object.assign(agentConfig.value, savedConfig);
}
// tools
if (!savedConfig.tools && configSchema.tools) {
agentConfig.value.tools = [...configSchema.tools];
}
//
Object.keys(configSchema).forEach(key => {
if (!['system_prompt', 'model', 'tools'].includes(key)) {
additionalConfig.value[key] = savedConfig[key] || configSchema[key] || '';
}
});
};
//
const saveConfig = () => {
//
const fullConfig = {
...agentConfig.value,
...additionalConfig.value
};
//
localStorage.setItem(`agent-config-${selectedAgentId.value}`, JSON.stringify(fullConfig));
//
message.success('配置已保存');
};
//
const resetConfig = () => {
loadAgentConfig();
message.info('配置已重置');
};
// localStorage
watch(
@ -69,6 +235,14 @@ watch(
}
);
//
watch(
() => selectedAgentId.value,
() => {
loadAgentConfig();
}
);
//
const fetchAgents = async () => {
try {
@ -81,6 +255,11 @@ const fetchAgents = async () => {
return acc;
}, {});
console.log("agents", agents.value);
//
if (selectedAgentId.value) {
loadAgentConfig();
}
} else {
console.error('获取智能体失败');
}
@ -89,25 +268,27 @@ const fetchAgents = async () => {
}
};
//
//
const toggleSidebar = () => {
state.isSidebarOpen = !state.isSidebarOpen;
};
//
const toggleConfigSidebar = (forceOpen) => {
if (forceOpen !== undefined) {
state.isConfigSidebarOpen = forceOpen;
} else {
state.isConfigSidebarOpen = !state.isConfigSidebarOpen;
}
};
//
const selectAgent = (agentId) => {
selectedAgentId.value = agentId;
//
localStorage.setItem('last-selected-agent', agentId);
};
//
const getAgentColor = (name) => {
//
const hash = name.split('').reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
return `hsl(${Math.abs(hash) % 360}, 70%, 60%)`;
//
loadAgentConfig();
};
//
@ -123,6 +304,9 @@ onMounted(async () => {
//
selectedAgentId.value = Object.keys(agents.value)[0];
}
//
loadAgentConfig();
});
</script>
@ -133,6 +317,7 @@ onMounted(async () => {
height: 100vh;
overflow: hidden;
--agent-sidebar-width: 230px;
--config-sidebar-width: 350px;
}
.sidebar {
@ -150,6 +335,44 @@ onMounted(async () => {
}
}
//
.config-sidebar {
width: 0;
max-width: var(--config-sidebar-width);
border-left: 1px solid var(--main-light-3);
background-color: var(--bg-sider);
box-sizing: content-box;
overflow-y: auto;
transition: width 0.3s ease;
overflow: hidden;
position: relative;
z-index: 100;
&.is-open {
width: var(--config-sidebar-width);
}
.config-form {
padding: 16px;
min-width: calc(var(--config-sidebar-width) - 16px);
overflow-y: auto;
max-height: calc(100vh - 100px);
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.no-agent-selected {
padding: 16px;
color: var(--gray-500);
text-align: center;
margin-top: 20px;
}
}
.sidebar-title {
font-weight: bold;
user-select: none;
@ -283,6 +506,21 @@ onMounted(async () => {
padding: 16px;
}
}
.config-sidebar {
position: absolute;
z-index: 101;
right: 0;
width: 0;
height: 100%;
border-radius: 16px 0 0 16px;
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
&.is-open {
width: 90%;
max-width: var(--config-sidebar-width);
}
}
}
</style>