Compare commits
6 Commits
1e8e0533fd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a4876ab00 | |||
| 52a9d02342 | |||
| b8944813cf | |||
| d9484f16c7 | |||
| 0e0f988264 | |||
| d72c6a3f25 |
@@ -36,6 +36,22 @@ Your workspace is at: {workspace_path}
|
||||
- Be helpful and concise
|
||||
- Think step by step when needed
|
||||
- Ask for clarification when the request is ambiguous
|
||||
|
||||
## Tool Usage Guidelines
|
||||
**IMPORTANT**: Only use tools when explicitly requested by the user:
|
||||
|
||||
**Use tools for**:
|
||||
- Searching the web for current information
|
||||
- Executing code or commands
|
||||
- Reading or writing files
|
||||
- Performing calculations
|
||||
|
||||
**DO NOT use tools for**:
|
||||
- Simple questions and greetings (e.g., "介绍一下武汉", "你好", "什么是AI")
|
||||
- General knowledge that you already know
|
||||
- Conversational responses
|
||||
|
||||
For simple informational questions, respond directly from your knowledge without calling any tools.
|
||||
"""
|
||||
|
||||
def build_messages(
|
||||
|
||||
278
core/agents/agent/intent_router.py
Normal file
278
core/agents/agent/intent_router.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""Intent recognition system for routing user requests."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntentType(Enum):
|
||||
"""Types of user intents."""
|
||||
SIMPLE = "simple" # Simple Q&A, no tools needed
|
||||
TOOL = "tool" # Needs tools (search, code, files, etc.)
|
||||
SKILL = "skill" # Needs specific domain skill
|
||||
TEAM = "team" # Needs multi-agent collaboration
|
||||
UNKNOWN = "unknown" # Cannot determine
|
||||
|
||||
|
||||
# Intent recognition prompt template
|
||||
INTENT_PROMPT = """Analyze the user's message and classify their intent.
|
||||
|
||||
Intent Types:
|
||||
- simple: General knowledge questions, greetings, casual conversation, simple Q&A
|
||||
Examples: "你好", "介绍一下武汉", "什么是AI", "今天天气怎么样"
|
||||
- tool: Requires external tools - web search, code execution, file operations, calculations
|
||||
Examples: "搜索最新的AI新闻", "帮我运行这段代码", "读取文件内容", "计算这个表达式"
|
||||
- skill: Requires specific domain skill (coding, design, analysis, etc.)
|
||||
Examples: "用Python写一个排序算法", "分析这段代码的性能", "创建一个网页"
|
||||
- team: Requires multiple agents working together
|
||||
Examples: "让设计agent和开发agent一起完成这个任务", "创建一个团队来完成这个项目"
|
||||
|
||||
Guidelines:
|
||||
- For greetings and simple questions, prefer "simple"
|
||||
- Only use "tool" when user explicitly asks for search, execution, or file operations
|
||||
- "introduce Wuhan" in Chinese is general knowledge - prefer "simple" unless user specifically asks for latest/current information
|
||||
- If ambiguous, prefer "simple" to avoid unnecessary tool calls
|
||||
|
||||
User message: {message}
|
||||
|
||||
Respond with only the intent type (simple/tool/skill/team), no explanation:"""
|
||||
|
||||
|
||||
class IntentRecognizer:
|
||||
"""Recognizes user intent to route requests appropriately."""
|
||||
|
||||
def __init__(self, llm_provider=None):
|
||||
"""Initialize intent recognizer.
|
||||
|
||||
Args:
|
||||
llm_provider: LLM provider for intent recognition
|
||||
"""
|
||||
self._llm_provider = llm_provider
|
||||
self._cache = {} # Simple cache for recent intents
|
||||
|
||||
def recognize(
|
||||
self,
|
||||
message: str,
|
||||
available_tools: list[str] | None = None,
|
||||
available_skills: list[str] | None = None,
|
||||
) -> IntentType:
|
||||
"""Recognize user intent.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
available_tools: List of available tool names
|
||||
available_skills: List of available skill names
|
||||
|
||||
Returns:
|
||||
Recognized intent type
|
||||
"""
|
||||
# Simple heuristics for common cases (fast path)
|
||||
intent = self._heuristic_recognition(message)
|
||||
if intent != IntentType.UNKNOWN:
|
||||
logger.info(f"Intent recognized (heuristic): {intent.value} for message: {message[:50]}...")
|
||||
return intent
|
||||
|
||||
# Use LLM for complex cases
|
||||
if self._llm_provider:
|
||||
return self._llm_recognition(message)
|
||||
|
||||
# Default to simple if no LLM
|
||||
return IntentType.SIMPLE
|
||||
|
||||
def _heuristic_recognition(self, message: str) -> IntentType:
|
||||
"""Fast heuristic-based intent recognition.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
|
||||
Returns:
|
||||
Recognized intent or UNKNOWN
|
||||
"""
|
||||
if not message:
|
||||
return IntentType.UNKNOWN
|
||||
|
||||
message_lower = message.lower().strip()
|
||||
|
||||
# Greetings
|
||||
greetings = ["你好", "hello", "hi", "嗨", "您好", "hey"]
|
||||
if any(g in message_lower for g in greetings) and len(message_lower) < 20:
|
||||
return IntentType.SIMPLE
|
||||
|
||||
# Simple questions patterns
|
||||
simple_patterns = [
|
||||
"什么是", "什么叫", "什么是",
|
||||
"介绍一下", "请介绍",
|
||||
"解释一下", "解释",
|
||||
"怎么样", "好不好",
|
||||
"是什么意思",
|
||||
"who are", "what is", "what's",
|
||||
"tell me about",
|
||||
]
|
||||
|
||||
# Check for simple patterns that don't require tools
|
||||
for pattern in simple_patterns:
|
||||
if pattern in message_lower:
|
||||
# But exclude if explicitly asking for current/latest/real-time
|
||||
if any(kw in message_lower for kw in ["最新", "现在", "current", "latest", "实时"]):
|
||||
return IntentType.UNKNOWN # Might need web search
|
||||
return IntentType.SIMPLE
|
||||
|
||||
# Explicit tool request patterns
|
||||
tool_patterns = [
|
||||
"搜索", "查找", "search",
|
||||
"执行", "运行", "run",
|
||||
"计算", "calculate",
|
||||
"帮我写代码", "write code",
|
||||
"读取", "读取", "read file",
|
||||
"创建文件", "write file",
|
||||
]
|
||||
|
||||
for pattern in tool_patterns:
|
||||
if pattern in message_lower:
|
||||
return IntentType.TOOL
|
||||
|
||||
# Skill patterns
|
||||
skill_patterns = [
|
||||
"用python", "用java", "用js",
|
||||
"写一个算法", "实现",
|
||||
"创建一个", "开发",
|
||||
"分析", "优化",
|
||||
]
|
||||
|
||||
for pattern in skill_patterns:
|
||||
if pattern in message_lower:
|
||||
return IntentType.SKILL
|
||||
|
||||
# Team patterns
|
||||
team_patterns = [
|
||||
"团队", "协作", "多个agent",
|
||||
"team", "collaborate", "一起",
|
||||
]
|
||||
|
||||
for pattern in team_patterns:
|
||||
if pattern in message_lower:
|
||||
return IntentType.TEAM
|
||||
|
||||
return IntentType.UNKNOWN
|
||||
|
||||
def _llm_recognition(self, message: str) -> IntentType:
|
||||
"""LLM-based intent recognition.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
|
||||
Returns:
|
||||
Recognized intent type
|
||||
"""
|
||||
try:
|
||||
prompt = INTENT_PROMPT.format(message=message)
|
||||
|
||||
# Use the LLM to classify intent
|
||||
response = self._llm_provider.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
max_tokens=50,
|
||||
)
|
||||
|
||||
content = response.content.strip().lower()
|
||||
|
||||
# Parse the response
|
||||
if "simple" in content:
|
||||
return IntentType.SIMPLE
|
||||
elif "tool" in content:
|
||||
return IntentType.TOOL
|
||||
elif "skill" in content:
|
||||
return IntentType.SKILL
|
||||
elif "team" in content:
|
||||
return IntentType.TEAM
|
||||
else:
|
||||
logger.warning(f"Unexpected intent response: {content}")
|
||||
return IntentType.SIMPLE # Default to simple
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM intent recognition failed: {e}")
|
||||
return IntentType.SIMPLE # Default to simple on error
|
||||
|
||||
|
||||
class IntentRouter:
|
||||
"""Routes requests based on recognized intent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
intent_recognizer: IntentRecognizer | None = None,
|
||||
use_llm_recognition: bool = True,
|
||||
):
|
||||
"""Initialize intent router.
|
||||
|
||||
Args:
|
||||
intent_recognizer: Intent recognizer instance
|
||||
use_llm_recognition: Whether to use LLM for complex cases
|
||||
"""
|
||||
self._recognizer = intent_recognizer
|
||||
self._use_llm = use_llm_recognition
|
||||
|
||||
def route(
|
||||
self,
|
||||
message: str,
|
||||
available_tools: list[str] | None = None,
|
||||
available_skills: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Route the user message based on intent.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
available_tools: List of available tool names
|
||||
available_skills: List of available skill names
|
||||
|
||||
Returns:
|
||||
Routing decision with intent type and suggested action
|
||||
"""
|
||||
# Recognize intent
|
||||
intent = self._recognizer.recognize(
|
||||
message,
|
||||
available_tools,
|
||||
available_skills,
|
||||
)
|
||||
|
||||
# Build routing decision
|
||||
decision = {
|
||||
"intent": intent.value,
|
||||
"action": self._get_action(intent),
|
||||
"message": message,
|
||||
}
|
||||
|
||||
logger.info(f"Routed message to {intent.value}: {message[:50]}...")
|
||||
|
||||
return decision
|
||||
|
||||
def _get_action(self, intent: IntentType) -> str:
|
||||
"""Get the action to take based on intent.
|
||||
|
||||
Args:
|
||||
intent: Recognized intent type
|
||||
|
||||
Returns:
|
||||
Action name
|
||||
"""
|
||||
return {
|
||||
IntentType.SIMPLE: "direct_response",
|
||||
IntentType.TOOL: "execute_tools",
|
||||
IntentType.SKILL: "execute_skill",
|
||||
IntentType.TEAM: "team_collaboration",
|
||||
IntentType.UNKNOWN: "direct_response", # Default to direct response
|
||||
}.get(intent, "direct_response")
|
||||
|
||||
|
||||
def create_intent_router(llm_provider=None) -> IntentRouter:
|
||||
"""Create an intent router with default settings.
|
||||
|
||||
Args:
|
||||
llm_provider: LLM provider for intent recognition
|
||||
|
||||
Returns:
|
||||
Configured IntentRouter instance
|
||||
"""
|
||||
recognizer = IntentRecognizer(llm_provider=llm_provider)
|
||||
return IntentRouter(intent_recognizer=recognizer)
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Callable, Awaitable, AsyncGenerator
|
||||
|
||||
from agents.agent.context import ContextBuilder
|
||||
from agents.agent.memory import AgentMemory
|
||||
from agents.agent.intent_router import IntentRouter, create_intent_router, IntentType
|
||||
from agents.llm import LLMProvider, LLMResponse, ProviderFactory
|
||||
from agents.tools import ToolRegistry
|
||||
|
||||
@@ -28,6 +29,7 @@ class AgentLoop:
|
||||
workspace: Path | None = None,
|
||||
max_iterations: int = 10,
|
||||
tools: ToolRegistry | None = None,
|
||||
enable_intent_routing: bool = True,
|
||||
):
|
||||
"""Initialize the agent loop.
|
||||
|
||||
@@ -37,16 +39,24 @@ class AgentLoop:
|
||||
workspace: Workspace directory for memory and configs
|
||||
max_iterations: Maximum tool call iterations
|
||||
tools: Tool registry (creates default if None)
|
||||
enable_intent_routing: Enable intent recognition and routing
|
||||
"""
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.workspace = workspace or Path.cwd()
|
||||
self.max_iterations = max_iterations
|
||||
self.tools = tools
|
||||
self.enable_intent_routing = enable_intent_routing
|
||||
|
||||
self.context = ContextBuilder(self.workspace)
|
||||
self.memory = AgentMemory(self.workspace)
|
||||
|
||||
# Initialize intent router
|
||||
if enable_intent_routing:
|
||||
self.intent_router = create_intent_router(llm_provider=provider)
|
||||
else:
|
||||
self.intent_router = None
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
message: str,
|
||||
@@ -79,10 +89,43 @@ class AgentLoop:
|
||||
"""
|
||||
history = history or []
|
||||
|
||||
# Intent recognition and routing
|
||||
intent_decision = None
|
||||
if self.intent_router and not history: # Only for first message in conversation
|
||||
try:
|
||||
tool_names = self.tools.tool_names if self.tools else []
|
||||
intent_decision = self.intent_router.route(
|
||||
message=message,
|
||||
available_tools=tool_names,
|
||||
)
|
||||
logger.info(f"Intent recognized: {intent_decision['intent']} -> {intent_decision['action']}")
|
||||
|
||||
# For simple intent, respond directly without tool loop
|
||||
if intent_decision["intent"] == IntentType.SIMPLE.value:
|
||||
# Build messages for direct response
|
||||
messages = self.context.build_messages(
|
||||
history=history,
|
||||
current_message=message,
|
||||
)
|
||||
# Call LLM without tools
|
||||
response = await self.provider.chat_with_retry(
|
||||
messages=messages,
|
||||
tools=None, # No tools for simple requests
|
||||
model=self.model,
|
||||
)
|
||||
content = self._strip_think(response.content) or "好的,让我来回答这个问题。"
|
||||
# Save to history
|
||||
self._save_history(session_key, messages, len(history))
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Intent routing failed: {e}, continuing with normal flow")
|
||||
|
||||
# Load history from session if session_key is provided
|
||||
if session_key and session_key != "default":
|
||||
loaded_history = self.memory.get_history(session_key, max_messages=20)
|
||||
if loaded_history:
|
||||
# Merge any split assistant messages
|
||||
loaded_history = self._merge_history_messages(loaded_history)
|
||||
logger.info(f"Loaded {len(loaded_history)} messages from session history")
|
||||
# Merge loaded history with provided history (loaded takes precedence if empty)
|
||||
if not history:
|
||||
@@ -155,10 +198,43 @@ class AgentLoop:
|
||||
"""
|
||||
history = history or []
|
||||
|
||||
# Intent recognition and routing
|
||||
intent_decision = None
|
||||
if self.intent_router and not history: # Only for first message in conversation
|
||||
try:
|
||||
tool_names = self.tools.tool_names if self.tools else []
|
||||
intent_decision = self.intent_router.route(
|
||||
message=message,
|
||||
available_tools=tool_names,
|
||||
)
|
||||
logger.info(f"Intent recognized: {intent_decision['intent']} -> {intent_decision['action']}")
|
||||
|
||||
# For simple intent, respond directly without tool loop
|
||||
if intent_decision["intent"] == IntentType.SIMPLE.value:
|
||||
# Build messages for direct response
|
||||
messages = self.context.build_messages(
|
||||
history=history,
|
||||
current_message=message,
|
||||
)
|
||||
# Call LLM without tools
|
||||
response = await self.provider.chat_with_retry(
|
||||
messages=messages,
|
||||
tools=None, # No tools for simple requests
|
||||
model=self.model,
|
||||
)
|
||||
content = self._strip_think(response.content) or "好的,让我来回答这个问题。"
|
||||
# Save to history
|
||||
self._save_history(session_key, messages, len(history))
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Intent routing failed: {e}, continuing with normal flow")
|
||||
|
||||
# Load history from session if session_key is provided
|
||||
if session_key and session_key != "default":
|
||||
loaded_history = self.memory.get_history(session_key, max_messages=20)
|
||||
if loaded_history:
|
||||
# Merge any split assistant messages
|
||||
loaded_history = self._merge_history_messages(loaded_history)
|
||||
logger.info(f"Loaded {len(loaded_history)} messages from session history")
|
||||
# Merge loaded history with provided history (loaded takes precedence if empty)
|
||||
if not history:
|
||||
@@ -334,6 +410,28 @@ class AgentLoop:
|
||||
|
||||
tool_defs = self.tools.get_definitions() if self.tools else []
|
||||
|
||||
# Intent recognition - determine if tools are needed before first LLM call
|
||||
user_message = ""
|
||||
for msg in messages:
|
||||
if msg.get("role") == "user":
|
||||
user_message = msg.get("content", "")
|
||||
break
|
||||
|
||||
# Apply intent recognition on first iteration
|
||||
if self.enable_intent_routing and self.intent_router and user_message:
|
||||
available_tools = [t.get("function", {}).get("name", "") for t in tool_defs] if tool_defs else []
|
||||
routing_decision = self.intent_router.route(
|
||||
user_message,
|
||||
available_tools=available_tools,
|
||||
)
|
||||
intent = routing_decision.get("intent", "simple")
|
||||
logger.info(f"Intent recognized: {intent} for message: {user_message[:50]}...")
|
||||
|
||||
# If simple intent, don't pass tools to reduce unnecessary tool calls
|
||||
if intent == "simple":
|
||||
tool_defs = []
|
||||
logger.info("Simple intent detected - disabling tool definitions for this request")
|
||||
|
||||
while iteration < self.max_iterations:
|
||||
iteration += 1
|
||||
|
||||
@@ -423,6 +521,28 @@ class AgentLoop:
|
||||
model = model or self.model
|
||||
tool_defs = self.tools.get_definitions() if self.tools else []
|
||||
|
||||
# Intent recognition - determine if tools are needed before first LLM call
|
||||
user_message = ""
|
||||
for msg in initial_messages:
|
||||
if msg.get("role") == "user":
|
||||
user_message = msg.get("content", "")
|
||||
break
|
||||
|
||||
# Apply intent recognition
|
||||
if self.enable_intent_routing and self.intent_router and user_message:
|
||||
available_tools = [t.get("function", {}).get("name", "") for t in tool_defs] if tool_defs else []
|
||||
routing_decision = self.intent_router.route(
|
||||
user_message,
|
||||
available_tools=available_tools,
|
||||
)
|
||||
intent = routing_decision.get("intent", "simple")
|
||||
logger.info(f"[stream] Intent recognized: {intent} for message: {user_message[:50]}...")
|
||||
|
||||
# If simple intent, don't pass tools to reduce unnecessary tool calls
|
||||
if intent == "simple":
|
||||
tool_defs = []
|
||||
logger.info("[stream] Simple intent detected - disabling tool definitions")
|
||||
|
||||
# First call to check for tool calls
|
||||
response = await provider.chat_with_retry(
|
||||
messages=initial_messages,
|
||||
@@ -490,6 +610,55 @@ class AgentLoop:
|
||||
return f'{tc.name}("{val[:40]}...")' if len(val) > 40 else f'{tc.name}("{val}")'
|
||||
return ", ".join(_fmt(tc) for tc in tool_calls)
|
||||
|
||||
@staticmethod
|
||||
def _merge_history_messages(messages: list[dict]) -> list[dict]:
|
||||
"""Merge adjacent assistant messages that have content and tool_calls separately.
|
||||
|
||||
When saving/loading history, assistant messages with both content and tool_calls
|
||||
might be split into multiple entries. This method merges them back together.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
|
||||
Returns:
|
||||
Merged list of messages
|
||||
"""
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
merged = []
|
||||
i = 0
|
||||
while i < len(messages):
|
||||
current = messages[i].copy()
|
||||
|
||||
# If current is an assistant message with tool_calls, check if next is
|
||||
# an assistant message with content (or vice versa)
|
||||
if current.get("role") == "assistant" and current.get("tool_calls"):
|
||||
# Look ahead for another assistant message to merge with
|
||||
j = i + 1
|
||||
while j < len(messages):
|
||||
next_msg = messages[j]
|
||||
if next_msg.get("role") == "assistant":
|
||||
# Merge content
|
||||
if next_msg.get("content") and not current.get("content"):
|
||||
current["content"] = next_msg.get("content")
|
||||
# Merge tool_calls (should already be in current)
|
||||
if next_msg.get("tool_calls") and not current.get("tool_calls"):
|
||||
current["tool_calls"] = next_msg.get("tool_calls")
|
||||
j += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# If we merged multiple messages, skip them
|
||||
if j > i + 1:
|
||||
logger.debug(f"Merged {j - i} assistant messages")
|
||||
i = j
|
||||
else:
|
||||
merged.append(current)
|
||||
i += 1
|
||||
|
||||
return merged
|
||||
|
||||
def _save_history(
|
||||
self,
|
||||
session_key: str,
|
||||
@@ -510,13 +679,18 @@ class AgentLoop:
|
||||
if role == "user" and content:
|
||||
self.memory.add_to_history("user", str(content)[:1000], session_key)
|
||||
elif role == "assistant":
|
||||
# Save assistant message content
|
||||
# Build a combined message with content and tool_calls
|
||||
msg_data = {}
|
||||
if content:
|
||||
self.memory.add_to_history("assistant", str(content)[:1000], session_key)
|
||||
# Save tool_calls if present (needed for multi-turn tool calls)
|
||||
msg_data["content"] = str(content)[:1000]
|
||||
if m.get("tool_calls"):
|
||||
tool_calls_str = json.dumps(m.get("tool_calls", []))
|
||||
self.memory.add_to_history("assistant", f"[tool_calls]{tool_calls_str}", session_key)
|
||||
msg_data["tool_calls"] = m.get("tool_calls", [])
|
||||
|
||||
# Save as a single JSON message with all data
|
||||
if msg_data:
|
||||
msg_str = json.dumps(msg_data)
|
||||
self.memory.add_to_history("assistant", msg_str, session_key)
|
||||
|
||||
# Save tool results (needed for multi-turn conversations)
|
||||
elif role == "tool":
|
||||
tool_call_id = m.get("tool_call_id", "")
|
||||
|
||||
@@ -537,7 +537,7 @@ class AgentMemory:
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check if content contains tool_calls or tool_result markers
|
||||
# Check if content contains tool_calls or tool_result markers, or is JSON
|
||||
# Format as Markdown (产品经理指定格式)
|
||||
entry_lines = [
|
||||
f"## 消息 {msg_count}",
|
||||
@@ -553,6 +553,19 @@ class AgentMemory:
|
||||
entry_lines.append(f"工具结果: {content[len('[tool_result]'):]}")
|
||||
entry_lines.append(f"内容: ")
|
||||
else:
|
||||
# Check if it's a JSON object (new format with content + tool_calls)
|
||||
try:
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict):
|
||||
# New JSON format: might have content and/or tool_calls
|
||||
if "content" in data:
|
||||
entry_lines.append(f"内容: {data['content']}")
|
||||
if "tool_calls" in data:
|
||||
entry_lines.append(f"工具调用: {json.dumps(data['tool_calls'])}")
|
||||
else:
|
||||
entry_lines.append(f"内容: {content}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# Not JSON, treat as regular content
|
||||
entry_lines.append(f"内容: {content}")
|
||||
|
||||
entry = "\n".join(entry_lines) + "\n\n"
|
||||
@@ -631,6 +644,9 @@ class AgentMemory:
|
||||
if line.startswith("工具调用:") and current_message is not None:
|
||||
tool_calls_json = line.split(":", 1)[1].strip()
|
||||
try:
|
||||
# Set role if not already set
|
||||
if not current_message.get("role"):
|
||||
current_message["role"] = "assistant"
|
||||
current_message["tool_calls"] = json.loads(tool_calls_json)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -641,6 +657,7 @@ class AgentMemory:
|
||||
tool_result_json = line.split(":", 1)[1].strip()
|
||||
try:
|
||||
tool_result = json.loads(tool_result_json)
|
||||
current_message["role"] = "tool" # Set role to tool
|
||||
current_message["tool_call_id"] = tool_result.get("tool_call_id", "")
|
||||
current_message["name"] = tool_result.get("name", "")
|
||||
current_message["content"] = tool_result.get("content", "")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""X-Agents API Module."""
|
||||
|
||||
from agents.api.routes import router
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
26
core/agents/api/server.py
Normal file
26
core/agents/api/server.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""X-Agents API Server."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, 'D:/Code/Project/X-Agents/core')
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from .routes import router
|
||||
|
||||
app = FastAPI(title="X-Agents API")
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include the router
|
||||
app.include_router(router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
@@ -275,7 +275,7 @@ class WebSearchTool(Tool):
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "Search the web for information using a search engine."
|
||||
return "Search the web for current information, real-time data, or information that is not in your training data. **Only use this when the user explicitly asks for** latest news, current events, real-time information, or specifically requests a web search. **DO NOT use for simple questions** like '介绍一下武汉', '什么是AI' - answer from your knowledge instead."
|
||||
|
||||
@property
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
|
||||
@@ -387,7 +387,14 @@ func main() {
|
||||
log.Println("Default tools initialized")
|
||||
}
|
||||
|
||||
// 4.3 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口)
|
||||
// 4.3 初始化团队成员智能体
|
||||
if err := agentService.InitTeamMembers(); err != nil {
|
||||
log.Printf("Warning: Failed to init team members: %v", err)
|
||||
} else {
|
||||
log.Println("Team members initialized")
|
||||
}
|
||||
|
||||
// 4.4 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口)
|
||||
// if err := skillService.InitSkills(); err != nil {
|
||||
// log.Printf("Warning: Failed to init skills: %v", err)
|
||||
// } else {
|
||||
@@ -405,7 +412,7 @@ func main() {
|
||||
toolHandler := handler.NewToolHandler(toolService)
|
||||
mcpHandler := handler.NewMCPHandler(mcpService)
|
||||
skillHandler := handler.NewSkillHandler(skillService)
|
||||
agentHandler := handler.NewAgentHandler(agentService)
|
||||
agentHandler := handler.NewAgentHandler(agentService, agentRepo)
|
||||
memoryHandler := handler.NewMemoryHandler(memoryService)
|
||||
sessionHandler := handler.NewSessionHandler(chatRepo, agentService)
|
||||
|
||||
@@ -590,6 +597,7 @@ func main() {
|
||||
{
|
||||
agentGroup.GET("/list", agentHandler.ListAgents)
|
||||
agentGroup.POST("/create", agentHandler.CreateAgent)
|
||||
agentGroup.POST("/init-team", agentHandler.InitTeamMembers)
|
||||
agentGroup.PUT("/:id/status", agentHandler.UpdateAgentStatus)
|
||||
agentGroup.PUT("/:id", agentHandler.UpdateAgent)
|
||||
agentGroup.DELETE("/:id", agentHandler.DeleteAgent)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 本地开发配置
|
||||
port: "8082"
|
||||
port: "8080"
|
||||
jwt_secret: "dev-secret-key"
|
||||
|
||||
# 数据库配置 (类型: mysql 或 sqlite)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"x-agents/server/internal/model"
|
||||
"x-agents/server/internal/repository"
|
||||
"x-agents/server/internal/service"
|
||||
"x-agents/server/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -12,22 +17,25 @@ import (
|
||||
// AgentHandler Agent 处理器
|
||||
type AgentHandler struct {
|
||||
agentService *service.AgentService
|
||||
agentRepo *repository.AgentRepository
|
||||
}
|
||||
|
||||
// NewAgentHandler 创建 Agent 处理器
|
||||
func NewAgentHandler(agentService *service.AgentService) *AgentHandler {
|
||||
func NewAgentHandler(agentService *service.AgentService, agentRepo *repository.AgentRepository) *AgentHandler {
|
||||
return &AgentHandler{
|
||||
agentService: agentService,
|
||||
agentRepo: agentRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// ChatRequest 对话请求
|
||||
type ChatRequest struct {
|
||||
AgentID string `json:"agent_id" binding:"required"` // 字符串类型
|
||||
AgentID string `json:"agent_id"` // 字符串类型,支持 UUID,可为空(当使用 mentioned_agent_ids 时)
|
||||
Message string `json:"message" binding:"required"`
|
||||
SessionID string `json:"session_id"`
|
||||
ModelID string `json:"model_id"`
|
||||
UseXBot bool `json:"use_xbot"`
|
||||
MentionedAgentIDs []string `json:"mentioned_agent_ids"` // @ 提及的智能体 ID 列表
|
||||
}
|
||||
|
||||
// ChatResponse 对话响应
|
||||
@@ -131,6 +139,29 @@ func (h *AgentHandler) ChatStream(c *gin.Context) {
|
||||
// 直接使用字符串类型的 agent_id,支持 UUID
|
||||
agentID := req.AgentID
|
||||
|
||||
// 优先使用前端传递的 mentioned_agent_ids
|
||||
if len(req.MentionedAgentIDs) > 0 {
|
||||
// 如果有多个 @ 提及,使用第一个
|
||||
mentionedAgentID := req.MentionedAgentIDs[0]
|
||||
log.Printf("[ChatStream] Using mentioned_agent_ids: %v", req.MentionedAgentIDs)
|
||||
agentID = mentionedAgentID
|
||||
// 清理消息,移除 @ 提及
|
||||
mentionParser := utils.NewMentionParser()
|
||||
req.Message = mentionParser.RemoveMentions(req.Message)
|
||||
} else if agentID == "" {
|
||||
// 兼容:解析消息中的 @ 提及(备用方案)
|
||||
mentionParser := utils.NewMentionParser()
|
||||
mentions := mentionParser.ParseMentions(req.Message)
|
||||
if len(mentions) > 0 {
|
||||
mentionedAgent := h.findAgentByName(mentions[0])
|
||||
if mentionedAgent != nil {
|
||||
log.Printf("[ChatStream] Detected @mention: %s, routing to agent: %s", mentions[0], mentionedAgent.ID)
|
||||
agentID = mentionedAgent.ID
|
||||
}
|
||||
req.Message = mentionParser.RemoveMentions(req.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 SSE 流
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
@@ -144,6 +175,37 @@ func (h *AgentHandler) ChatStream(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// findAgentByName 根据用户名查找智能体
|
||||
func (h *AgentHandler) findAgentByName(name string) *model.Agent {
|
||||
log.Printf("[findAgentByName] Searching for agent: %s, agentRepo: %v", name, h.agentRepo)
|
||||
|
||||
if h.agentRepo == nil {
|
||||
log.Printf("[findAgentByName] ERROR: agentRepo is nil!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 先尝试精确匹配
|
||||
agents, err := h.agentRepo.FindAll()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, agent := range agents {
|
||||
if agent.Name == name {
|
||||
return &agent
|
||||
}
|
||||
}
|
||||
|
||||
// 再尝试模糊匹配(忽略大小写)
|
||||
for _, agent := range agents {
|
||||
if strings.Contains(strings.ToLower(agent.Name), strings.ToLower(name)) {
|
||||
return &agent
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TeamChatRequest 多智能体群聊请求
|
||||
type TeamChatRequest struct {
|
||||
SupervisorAgentID int `json:"supervisor_agent_id" binding:"required"`
|
||||
@@ -236,6 +298,30 @@ func (h *AgentHandler) CreateAgent(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// InitTeamMembersResponse 初始化团队成员响应
|
||||
type InitTeamMembersResponse struct {
|
||||
Message string `json:"message"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// InitTeamMembers 初始化团队成员智能体
|
||||
// @Summary 初始化团队成员智能体
|
||||
// @Tags 智能体管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} InitTeamMembersResponse
|
||||
// @Router /api/agent/init-team [post]
|
||||
func (h *AgentHandler) InitTeamMembers(c *gin.Context) {
|
||||
if err := h.agentService.InitTeamMembers(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, InitTeamMembersResponse{
|
||||
Message: "Team members initialized successfully",
|
||||
Count: 1, // 小荣
|
||||
})
|
||||
}
|
||||
|
||||
// ListAgents 获取智能体列表
|
||||
// @Summary 获取智能体列表
|
||||
// @Tags 智能体管理
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) {
|
||||
log.Printf("[AgentService] Sending to Python: model_id=%s, api_key=%s, base_url=%s, provider=%s, model=%s",
|
||||
req.ModelID, apiKeyPreview, req.BaseURL, req.ModelProvider, req.ModelName)
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/agent/chat", s.pythonURL)
|
||||
url := fmt.Sprintf("%s/agent/chat", s.pythonURL)
|
||||
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
@@ -155,7 +155,7 @@ func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) {
|
||||
|
||||
// TeamChat 多智能体群聊
|
||||
func (s *AgentService) TeamChat(req TeamChatRequest) (*TeamChatResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/agent/team/chat", s.pythonURL)
|
||||
url := fmt.Sprintf("%s/agent/team/chat", s.pythonURL)
|
||||
|
||||
// 设置默认策略
|
||||
if req.Strategy == "" {
|
||||
@@ -233,7 +233,7 @@ func (s *AgentService) ChatStream(c interface{}, agentID string, message, sessio
|
||||
log.Printf("[ChatStream] modelID is empty or modelRepo is nil: modelID=%s, modelRepo=%v", modelID, s.modelRepo != nil)
|
||||
}
|
||||
|
||||
streamURL := fmt.Sprintf("%s/api/v1/agent/chat/stream", s.pythonURL)
|
||||
streamURL := fmt.Sprintf("%s/agent/chat/stream", s.pythonURL)
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
@@ -365,6 +365,89 @@ func (s *AgentService) CreateAgent(req CreateAgentRequest, userID int) (*CreateA
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TeamMemberInitRequest 团队成员初始化请求
|
||||
type TeamMemberInitRequest struct {
|
||||
Name string
|
||||
Description string
|
||||
Avatar string
|
||||
Skills []string
|
||||
RoleDescription string
|
||||
}
|
||||
|
||||
// InitTeamMembers 初始化团队成员智能体
|
||||
func (s *AgentService) InitTeamMembers() error {
|
||||
if s.agentRepo == nil {
|
||||
log.Printf("[AgentService] InitTeamMembers: agentRepo is nil!")
|
||||
return fmt.Errorf("agent repository not initialized")
|
||||
}
|
||||
|
||||
// 骚人开发组团队成员配置
|
||||
teamMembers := []TeamMemberInitRequest{
|
||||
{
|
||||
Name: "小荣",
|
||||
Description: "前端开发工程师 - 骚人开发组成员",
|
||||
Avatar: "👨💻",
|
||||
Skills: []string{"Vue 3", "TypeScript", "Element Plus", "Tailwind CSS"},
|
||||
RoleDescription: `你叫小荣,是骚人开发组的前端开发工程师。你细心认真,善于沟通。
|
||||
|
||||
技能专长:
|
||||
- Vue 3 框架开发
|
||||
- TypeScript 类型系统
|
||||
- Element Plus 组件库
|
||||
- Tailwind CSS 样式框架
|
||||
|
||||
性格特点:
|
||||
- 细心认真,注重代码质量
|
||||
- 善于与团队成员沟通协作
|
||||
- 积极解决前端技术难题`,
|
||||
},
|
||||
}
|
||||
|
||||
// 检查是否已存在同名智能体
|
||||
for _, member := range teamMembers {
|
||||
existingAgents, err := s.agentRepo.FindAll()
|
||||
if err != nil {
|
||||
log.Printf("[AgentService] InitTeamMembers: failed to list agents: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
exists := false
|
||||
for _, a := range existingAgents {
|
||||
if a.Name == member.Name {
|
||||
exists = true
|
||||
log.Printf("[AgentService] InitTeamMembers: agent %s already exists, skipping", member.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// 创建智能体
|
||||
agent := &model.Agent{
|
||||
ID: uuid.New().String(),
|
||||
Name: member.Name,
|
||||
Description: member.Description,
|
||||
OwnerID: "1", // 系统管理员
|
||||
Avatar: member.Avatar,
|
||||
Skills: member.Skills,
|
||||
RoleDescription: member.RoleDescription,
|
||||
ModelProvider: "anthropic",
|
||||
ModelName: "claude-sonnet-4-20250514",
|
||||
IsActive: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.agentRepo.Create(agent); err != nil {
|
||||
log.Printf("[AgentService] InitTeamMembers: failed to create agent %s: %v", member.Name, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[AgentService] InitTeamMembers: created agent %s (ID: %s)", member.Name, agent.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAgentsResponse 获取智能体列表响应
|
||||
type ListAgentsResponse struct {
|
||||
Agents []interface{} `json:"agents"`
|
||||
|
||||
@@ -1,15 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
loading: boolean
|
||||
agents?: { id: string | number; name: string; avatar: string }[]
|
||||
mentionedAgents?: { id: string | number; name: string; avatar: string }[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'send'): void
|
||||
(e: 'triggerMention'): void
|
||||
(e: 'removeMention', agentId: string | number): void
|
||||
}>()
|
||||
|
||||
const showMentionPopup = ref(false)
|
||||
const lastAtPosition = ref(-1)
|
||||
const selectedIndex = ref(0)
|
||||
|
||||
// 过滤后的智能体列表(排除已提及的)
|
||||
const filteredAgents = computed(() => {
|
||||
if (!props.agents) return []
|
||||
const mentionedIds = props.mentionedAgents?.map(a => a.id) || []
|
||||
return props.agents.filter(a => !mentionedIds.includes(a.id))
|
||||
})
|
||||
|
||||
// 解析消息中的 @ 提及
|
||||
const parseMentions = (text: string) => {
|
||||
const mentions: { id: string | number; name: string; avatar: string }[] = []
|
||||
const regex = /@(\S+)/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const name = match[1]
|
||||
const agent = props.agents?.find(a => a.name === name)
|
||||
if (agent && !mentions.find(m => m.id === agent.id)) {
|
||||
mentions.push(agent)
|
||||
}
|
||||
}
|
||||
|
||||
return mentions
|
||||
}
|
||||
|
||||
// 监听输入
|
||||
const handleInput = (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
const value = target.value
|
||||
const cursorPos = target.selectionStart
|
||||
|
||||
emit('update:modelValue', value)
|
||||
autoResize(e)
|
||||
|
||||
// 检测是否输入了 @
|
||||
const textBeforeCursor = value.slice(0, cursorPos)
|
||||
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
|
||||
|
||||
if (lastAtIndex !== -1) {
|
||||
// 检查 @ 后面是否有空格或是否在单词中间
|
||||
const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1)
|
||||
if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
||||
showMentionPopup.value = true
|
||||
lastAtPosition.value = lastAtIndex
|
||||
selectedIndex.value = 0
|
||||
emit('triggerMention')
|
||||
} else {
|
||||
showMentionPopup.value = false
|
||||
}
|
||||
} else {
|
||||
showMentionPopup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择智能体
|
||||
const selectAgent = (agent: { id: string | number; name: string; avatar: string }) => {
|
||||
if (!props.modelValue || lastAtPosition.value === -1) return
|
||||
|
||||
// 获取光标位置前的文本和后的文本
|
||||
const beforeAt = props.modelValue.slice(0, lastAtPosition.value)
|
||||
const afterCursor = props.modelValue.slice((document.querySelector('.chat-input-textarea') as HTMLTextAreaElement)?.selectionStart || 0)
|
||||
|
||||
// 替换 @xxx 为 @智能体名
|
||||
const newValue = beforeAt + '@' + agent.name + ' ' + afterCursor
|
||||
|
||||
emit('update:modelValue', newValue)
|
||||
showMentionPopup.value = false
|
||||
lastAtPosition.value = -1
|
||||
selectedIndex.value = 0
|
||||
|
||||
// 聚焦输入框
|
||||
setTimeout(() => {
|
||||
const textarea = document.querySelector('.chat-input-textarea') as HTMLTextAreaElement
|
||||
if (textarea) {
|
||||
textarea.focus()
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// 移除提及
|
||||
const removeMention = (agentId: string | number) => {
|
||||
emit('removeMention', agentId)
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
// @ 提及弹窗打开时处理方向键
|
||||
if (showMentionPopup.value && filteredAgents.value.length > 0) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = (selectedIndex.value + 1) % filteredAgents.value.length
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = selectedIndex.value === 0
|
||||
? filteredAgents.value.length - 1
|
||||
: selectedIndex.value - 1
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const agent = filteredAgents.value[selectedIndex.value]
|
||||
if (agent) {
|
||||
selectAgent(agent)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
showMentionPopup.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
emit('send')
|
||||
@@ -26,6 +148,26 @@ const autoResize = (e: Event) => {
|
||||
<template>
|
||||
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- 已提及的智能体显示 -->
|
||||
<div v-if="mentionedAgents && mentionedAgents.length > 0" class="flex flex-wrap gap-2 mb-3">
|
||||
<div
|
||||
v-for="agent in mentionedAgents"
|
||||
:key="agent.id"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 bg-orange-500/20 border border-orange-500/30 rounded-lg text-sm"
|
||||
>
|
||||
<span>{{ agent.avatar }}</span>
|
||||
<span class="text-orange-400">@{{ agent.name }}</span>
|
||||
<button
|
||||
@click="removeMention(agent.id)"
|
||||
class="ml-1 text-orange-400/60 hover:text-orange-400"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative bg-[#12121a] rounded-2xl border border-white/[0.08] focus-within:border-orange-500/40 focus-within:shadow-[0_0_30px_rgba(249,115,22,0.08)] transition-all duration-300">
|
||||
<!-- 附件按钮 -->
|
||||
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/25 hover:text-orange-400 transition-colors p-1">
|
||||
@@ -37,13 +179,33 @@ const autoResize = (e: Event) => {
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value); autoResize($event)"
|
||||
@input="handleInput"
|
||||
@keydown="handleKeydown"
|
||||
placeholder="发送消息..."
|
||||
placeholder="输入 @ 提及智能体..."
|
||||
rows="1"
|
||||
class="chat-input-textarea w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
|
||||
></textarea>
|
||||
|
||||
<!-- @ 提及弹窗 -->
|
||||
<div
|
||||
v-if="showMentionPopup && filteredAgents.length > 0"
|
||||
class="absolute left-0 bottom-full mb-2 w-64 bg-[#1a1a24] border border-white/10 rounded-xl shadow-xl overflow-hidden z-50"
|
||||
>
|
||||
<div class="p-2">
|
||||
<div class="text-xs text-white/40 px-2 py-1">选择智能体</div>
|
||||
<div
|
||||
v-for="(agent, index) in filteredAgents"
|
||||
:key="agent.id"
|
||||
@click="selectAgent(agent)"
|
||||
class="flex items-center gap-2 px-2 py-2 rounded-lg cursor-pointer transition-colors"
|
||||
:class="index === selectedIndex ? 'bg-orange-500/20' : 'hover:bg-white/5'"
|
||||
>
|
||||
<span class="text-lg">{{ agent.avatar }}</span>
|
||||
<span class="text-white text-sm">{{ agent.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
@click="emit('send')"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useChat } from './chat/chat'
|
||||
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
||||
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
||||
@@ -45,6 +46,27 @@ const {
|
||||
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// @ 提及的智能体
|
||||
const mentionedAgents = ref<{ id: string | number; name: string; avatar: string }[]>([])
|
||||
|
||||
// 触发 @ 提及
|
||||
const onTriggerMention = () => {
|
||||
// 可以在这里打开智能体选择弹窗,或显示提示
|
||||
}
|
||||
|
||||
// 移除 @ 提及
|
||||
const onRemoveMention = (agentId: string | number) => {
|
||||
const index = mentionedAgents.value.findIndex(a => a.id === agentId)
|
||||
if (index > -1) {
|
||||
mentionedAgents.value.splice(index, 1)
|
||||
}
|
||||
// 从输入框中移除 @ 提及
|
||||
const agent = chatAgents.value.find(a => a.id === agentId)
|
||||
if (agent) {
|
||||
inputMessage.value = inputMessage.value.replace(`@${agent.name}`, '')
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 API 请求体
|
||||
const buildRequestBody = (userContent: string) => {
|
||||
const requestBody: any = {
|
||||
@@ -60,26 +82,56 @@ const buildRequestBody = (userContent: string) => {
|
||||
requestBody.session_id = currentSessionId.value
|
||||
}
|
||||
|
||||
// 添加 @ 提及的智能体 ID
|
||||
if (mentionedAgents.value.length > 0) {
|
||||
requestBody.mentioned_agent_ids = mentionedAgents.value.map(a => String(a.id))
|
||||
}
|
||||
|
||||
return requestBody
|
||||
}
|
||||
|
||||
// 解析流式响应数据
|
||||
// 支持格式: data: "content" (JSON字符串) 或 data: {"content": "xxx"} (JSON对象)
|
||||
const parseStreamData = (rawData: string): string => {
|
||||
console.log('[Chat] parseStreamData 原始数据:', rawData)
|
||||
|
||||
if (!rawData || rawData === '[DONE]') return ''
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawData)
|
||||
console.log('[Chat] parseStreamData 解析结果:', parsed, '类型:', typeof parsed)
|
||||
|
||||
// 如果解析结果是字符串(JSON字符串形式),直接返回
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed
|
||||
}
|
||||
return parsed.content || parsed.delta?.content || ''
|
||||
} catch {
|
||||
|
||||
// 如果是对象,尝试获取 content 或 delta.content
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
// 兼容多种格式: content, delta.content, text, message.content
|
||||
return parsed.content || parsed.delta?.content || parsed.text || parsed.message?.content || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch (e) {
|
||||
console.error('[Chat] parseStreamData 解析错误:', e)
|
||||
// 解析失败时,尝试直接返回原始数据(可能是未转义的纯文本)
|
||||
if (rawData && rawData.length > 0) {
|
||||
// 尝试移除首尾空格和引号
|
||||
const trimmed = rawData.trim()
|
||||
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||
return trimmed.slice(1, -1)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
const handleStreamResponse = async (response: Response) => {
|
||||
console.log('[Chat] handleStreamResponse 开始处理流式响应, status:', response.status)
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
@@ -97,7 +149,11 @@ const handleStreamResponse = async (response: Response) => {
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const content = parseStreamData(line.slice(6).trim())
|
||||
const dataPart = line.slice(6).trim()
|
||||
console.log('[Chat] 流式数据行:', line)
|
||||
console.log('[Chat] 流式数据部分:', dataPart)
|
||||
const content = parseStreamData(dataPart)
|
||||
console.log('[Chat] 解析后内容:', content)
|
||||
if (content) {
|
||||
messages.value[aiMessageIndex].content += content
|
||||
await nextTick()
|
||||
@@ -188,6 +244,11 @@ const handleSelectModel = (model: any) => {
|
||||
showModelDropdown.value = false
|
||||
}
|
||||
|
||||
// 打开群聊选择器
|
||||
const openGroupChat = () => {
|
||||
openAgentSelector('group')
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
const handleDeleteSession = async (session: any) => {
|
||||
try {
|
||||
@@ -236,15 +297,17 @@ const generateSessionTitle = async () => {
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim() || isLoading.value) return
|
||||
|
||||
// 如果没有会话,提示用户先选择智能体
|
||||
if (!currentSessionId.value) {
|
||||
ElMessage.warning('请先选择或创建一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
const userContent = inputMessage.value.trim()
|
||||
inputMessage.value = ''
|
||||
mentionedAgents.value = []
|
||||
resetInputHeight()
|
||||
|
||||
if (!currentSessionId.value) {
|
||||
const session = await createSession()
|
||||
if (!session) return
|
||||
}
|
||||
|
||||
const userMessage = createUserMessage(userContent)
|
||||
messages.value.push(userMessage)
|
||||
await saveMessage('user', userContent)
|
||||
@@ -287,6 +350,7 @@ onUnmounted(() => {
|
||||
<div class="flex-1 flex flex-col bg-[#09090b]">
|
||||
<!-- 顶部栏 -->
|
||||
<ChatHeader
|
||||
v-if="currentSessionId"
|
||||
:selected-agent="selectedAgent"
|
||||
:chat-models="chatModels"
|
||||
:selected-model="selectedModel"
|
||||
@@ -301,8 +365,26 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 消息区域 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
||||
<!-- 空状态欢迎提示 -->
|
||||
<div v-if="messages.length === 0" class="h-full flex items-center justify-center">
|
||||
<!-- 无会话时显示引导界面 -->
|
||||
<div v-if="!currentSessionId" class="h-full flex items-center justify-center empty-chat">
|
||||
<div class="text-center" style="position: relative; z-index: 1;">
|
||||
<div class="empty-logo">🧠</div>
|
||||
<h2 class="empty-title">欢迎使用 X-Agents</h2>
|
||||
<p class="empty-desc">与智能 AI 助手对话,获取专业解答与创意灵感</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<button @click="newChat" class="empty-btn">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
开始新对话
|
||||
</button>
|
||||
<button @click="openGroupChat" class="empty-btn empty-btn-secondary">
|
||||
<i class="fa-solid fa-users mr-2"></i>
|
||||
开始群聊
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 有会话但无消息时显示欢迎提示 -->
|
||||
<div v-else-if="messages.length === 0" class="h-full flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-5xl mb-4">{{ selectedAgent?.avatar || '🧠' }}</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">和 {{ selectedAgent?.name || 'AI' }} 开始对话</h2>
|
||||
@@ -320,16 +402,22 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<!-- 输入区域 - 仅在有会话时显示 -->
|
||||
<ChatInput
|
||||
v-if="currentSessionId"
|
||||
v-model="inputMessage"
|
||||
:loading="isLoading"
|
||||
:agents="chatAgents"
|
||||
:mentioned-agents="mentionedAgents"
|
||||
@send="sendMessage"
|
||||
@trigger-mention="onTriggerMention"
|
||||
@remove-mention="onRemoveMention"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 -->
|
||||
<!-- 右侧边栏 - 仅在有会话时显示 -->
|
||||
<ChatSidebar
|
||||
v-if="currentSessionId"
|
||||
:collapsed="sidebarCollapsed"
|
||||
:chat-agents="chatAgents"
|
||||
:selected-agent="selectedAgent"
|
||||
|
||||
@@ -49,3 +49,159 @@
|
||||
.agent-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 空会话页面样式 */
|
||||
.empty-chat {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-chat::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(249, 115, 22, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 70%, rgba(239, 68, 68, 0.06) 0%, transparent 50%);
|
||||
animation: bgFloat 20s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes bgFloat {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||
50% { transform: translate(-2%, -2%) rotate(1deg); }
|
||||
}
|
||||
|
||||
.empty-logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(239, 68, 68, 0.1));
|
||||
border-radius: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48px;
|
||||
animation: logoFloat 3s ease-in-out infinite;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.7) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 15px;
|
||||
margin-bottom: 32px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
padding: 14px 32px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 24px rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
|
||||
.empty-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
.empty-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.empty-btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.empty-btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 推荐智能体卡片 */
|
||||
.recommend-section {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.recommend-title {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.recommend-cards {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.recommend-card {
|
||||
width: 160px;
|
||||
padding: 20px 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recommend-card:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.recommend-avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin: 0 auto 12px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.recommend-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.recommend-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ export function useChat() {
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
currentSessionId.value = session.id
|
||||
saveSessionId(session.id)
|
||||
return session
|
||||
} catch {
|
||||
return null
|
||||
@@ -445,7 +445,24 @@ export function useChat() {
|
||||
// 调用后端 API 创建群聊
|
||||
const group = await createGroup(name, agentIds)
|
||||
|
||||
if (!group) {
|
||||
if (group) {
|
||||
// 创建成功,刷新群聊列表
|
||||
await fetchGroups()
|
||||
|
||||
// 创建群聊会话
|
||||
const session = await createSession(name)
|
||||
if (session) {
|
||||
saveSessionId(session.id)
|
||||
|
||||
// 显示群聊欢迎消息
|
||||
messages.value = [
|
||||
{ id: Date.now(), role: 'assistant', content: `你好!欢迎进入群聊 "${name}",${selectedAgents.value.length} 位智能体已加入。`, timestamp: new Date() }
|
||||
]
|
||||
|
||||
// 保存欢迎消息
|
||||
await saveMessage('assistant', messages.value[0].content)
|
||||
}
|
||||
} else {
|
||||
// 如果 API 调用失败,使用本地数据
|
||||
groupChats.value.unshift({
|
||||
id: Date.now(),
|
||||
@@ -508,18 +525,38 @@ export function useChat() {
|
||||
}
|
||||
|
||||
// 选择历史对话
|
||||
// 保存会话 ID 到 localStorage
|
||||
const saveSessionId = (sessionId: string) => {
|
||||
localStorage.setItem('current_session_id', sessionId)
|
||||
currentSessionId.value = sessionId
|
||||
}
|
||||
|
||||
// 从 localStorage 恢复会话
|
||||
const restoreSession = async () => {
|
||||
const savedSessionId = localStorage.getItem('current_session_id')
|
||||
if (!savedSessionId) return
|
||||
|
||||
const session = chatSessions.value.find(s => s.id === savedSessionId)
|
||||
if (session) {
|
||||
await selectSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
const selectSession = async (session: ChatSession) => {
|
||||
const agent = chatAgents.value.find(a => a.id === session.agent_id)
|
||||
if (agent) {
|
||||
selectedAgent.value = agent
|
||||
}
|
||||
|
||||
currentSessionId.value = session.id
|
||||
saveSessionId(session.id)
|
||||
await fetchSessionMessages(session.id)
|
||||
}
|
||||
|
||||
// 新建聊天 - 先打开智能体选择器
|
||||
const newChat = () => {
|
||||
// 清除当前会话 ID(新建会话时会重新设置)
|
||||
currentSessionId.value = null
|
||||
localStorage.removeItem('current_session_id')
|
||||
// 打开智能体选择器,让用户选择智能体
|
||||
openAgentSelector('single')
|
||||
}
|
||||
@@ -561,12 +598,15 @@ export function useChat() {
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
const init = async () => {
|
||||
fetchModels()
|
||||
fetchAgents()
|
||||
fetchSessions()
|
||||
await fetchSessions()
|
||||
fetchGroups()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
||||
// 恢复之前选中的会话
|
||||
await restoreSession()
|
||||
}
|
||||
|
||||
// 清理
|
||||
|
||||
Reference in New Issue
Block a user