mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-03 14:40:13 +08:00
feat: Implement AI Agent with enhanced tool processing capabilities (#89)
* feat: Implement AI Agent with tool processing capabilities - Added tools for listing and running processors in the agent. - Created data models for agent chat requests and tool calls. - Developed API integration for agent chat and streaming responses. - Built the AI Agent widget with a user interface for interaction. - Styled the agent components for better user experience. * feat: 增强 AI 助手工具功能,添加文件操作和搜索功能,更新界面显示 * feat: 更新 AI 助手组件 * feat: 更新 AiAgentWidget 组件样式,调整背景和边距以提升界面一致性
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from typing import List, Sequence, Tuple
|
||||
from typing import Any, AsyncIterator, Dict, List, Sequence, Tuple
|
||||
|
||||
from models.database import AIModel, AIProvider
|
||||
from domain.ai.service import AIProviderService
|
||||
@@ -243,3 +245,195 @@ async def _rerank_with_gemini(
|
||||
except (TypeError, ValueError):
|
||||
scores.append(0.0)
|
||||
return scores
|
||||
|
||||
|
||||
async def chat_completion(
|
||||
messages: List[Dict[str, Any]],
|
||||
*,
|
||||
ability: str = "chat",
|
||||
tools: List[Dict[str, Any]] | None = None,
|
||||
tool_choice: Any | None = None,
|
||||
temperature: float | None = None,
|
||||
timeout: float = 60.0,
|
||||
) -> Dict[str, Any]:
|
||||
model, provider = await _require_model(ability)
|
||||
if provider.api_format != "openai":
|
||||
raise MissingModelError("当前仅支持 OpenAI 兼容接口的对话模型。")
|
||||
return await _chat_with_openai(
|
||||
provider,
|
||||
model,
|
||||
messages,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
temperature=temperature,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
async def _chat_with_openai(
|
||||
provider: AIProvider,
|
||||
model: AIModel,
|
||||
messages: List[Dict[str, Any]],
|
||||
*,
|
||||
tools: List[Dict[str, Any]] | None,
|
||||
tool_choice: Any | None,
|
||||
temperature: float | None,
|
||||
timeout: float,
|
||||
) -> Dict[str, Any]:
|
||||
url = _openai_endpoint(provider, "/chat/completions")
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model.name,
|
||||
"messages": messages,
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
payload["tool_choice"] = tool_choice or "auto"
|
||||
if temperature is not None:
|
||||
payload["temperature"] = float(temperature)
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(url, headers=_openai_headers(provider), json=payload)
|
||||
response.raise_for_status()
|
||||
body = response.json()
|
||||
|
||||
choices = body.get("choices") or []
|
||||
if not choices:
|
||||
raise RuntimeError("对话接口返回为空")
|
||||
message = choices[0].get("message")
|
||||
if not isinstance(message, dict):
|
||||
raise RuntimeError("对话接口返回格式异常")
|
||||
return message
|
||||
|
||||
|
||||
async def chat_completion_stream(
|
||||
messages: List[Dict[str, Any]],
|
||||
*,
|
||||
ability: str = "chat",
|
||||
tools: List[Dict[str, Any]] | None = None,
|
||||
tool_choice: Any | None = None,
|
||||
temperature: float | None = None,
|
||||
timeout: float = 60.0,
|
||||
) -> AsyncIterator[Dict[str, Any]]:
|
||||
model, provider = await _require_model(ability)
|
||||
if provider.api_format != "openai":
|
||||
raise MissingModelError("当前仅支持 OpenAI 兼容接口的对话模型。")
|
||||
async for event in _chat_stream_with_openai(
|
||||
provider,
|
||||
model,
|
||||
messages,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
temperature=temperature,
|
||||
timeout=timeout,
|
||||
):
|
||||
yield event
|
||||
|
||||
|
||||
async def _chat_stream_with_openai(
|
||||
provider: AIProvider,
|
||||
model: AIModel,
|
||||
messages: List[Dict[str, Any]],
|
||||
*,
|
||||
tools: List[Dict[str, Any]] | None,
|
||||
tool_choice: Any | None,
|
||||
temperature: float | None,
|
||||
timeout: float,
|
||||
) -> AsyncIterator[Dict[str, Any]]:
|
||||
url = _openai_endpoint(provider, "/chat/completions")
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model.name,
|
||||
"messages": messages,
|
||||
"stream": True,
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
payload["tool_choice"] = tool_choice or "auto"
|
||||
if temperature is not None:
|
||||
payload["temperature"] = float(temperature)
|
||||
|
||||
content_parts: List[str] = []
|
||||
tool_call_map: Dict[int, Dict[str, Any]] = {}
|
||||
role = "assistant"
|
||||
finish_reason: str | None = None
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
async with client.stream("POST", url, headers=_openai_headers(provider), json=payload) as response:
|
||||
response.raise_for_status()
|
||||
async for line in response.aiter_lines():
|
||||
if not line:
|
||||
continue
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
data = line[5:].strip()
|
||||
if not data:
|
||||
continue
|
||||
if data == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
choices = chunk.get("choices") or []
|
||||
if not choices:
|
||||
continue
|
||||
choice = choices[0] if isinstance(choices[0], dict) else {}
|
||||
delta = choice.get("delta") if isinstance(choice, dict) else None
|
||||
delta = delta if isinstance(delta, dict) else {}
|
||||
|
||||
if isinstance(delta.get("role"), str):
|
||||
role = delta["role"]
|
||||
|
||||
delta_content = delta.get("content")
|
||||
if isinstance(delta_content, str) and delta_content:
|
||||
content_parts.append(delta_content)
|
||||
yield {"type": "delta", "delta": delta_content}
|
||||
|
||||
delta_tool_calls = delta.get("tool_calls")
|
||||
if isinstance(delta_tool_calls, list):
|
||||
for item in delta_tool_calls:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
idx = item.get("index")
|
||||
if not isinstance(idx, int):
|
||||
continue
|
||||
entry = tool_call_map.setdefault(
|
||||
idx,
|
||||
{"id": None, "type": None, "function": {"name": None, "arguments": ""}},
|
||||
)
|
||||
if isinstance(item.get("id"), str) and item["id"].strip():
|
||||
entry["id"] = item["id"]
|
||||
if isinstance(item.get("type"), str) and item["type"].strip():
|
||||
entry["type"] = item["type"]
|
||||
fn = item.get("function")
|
||||
if isinstance(fn, dict):
|
||||
if isinstance(fn.get("name"), str) and fn["name"].strip():
|
||||
entry["function"]["name"] = fn["name"]
|
||||
args_part = fn.get("arguments")
|
||||
if isinstance(args_part, str) and args_part:
|
||||
entry["function"]["arguments"] += args_part
|
||||
|
||||
fr = choice.get("finish_reason") if isinstance(choice, dict) else None
|
||||
if isinstance(fr, str) and fr:
|
||||
finish_reason = fr
|
||||
|
||||
content = "".join(content_parts)
|
||||
message: Dict[str, Any] = {"role": role, "content": content}
|
||||
if tool_call_map:
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
for idx in sorted(tool_call_map.keys()):
|
||||
item = tool_call_map[idx]
|
||||
fn = item.get("function") if isinstance(item.get("function"), dict) else {}
|
||||
call_id = item.get("id") if isinstance(item.get("id"), str) and item.get("id") else f"call_{idx}"
|
||||
call_type = item.get("type") if isinstance(item.get("type"), str) and item.get("type") else "function"
|
||||
tool_calls.append({
|
||||
"id": call_id,
|
||||
"type": call_type,
|
||||
"function": {
|
||||
"name": fn.get("name") or "",
|
||||
"arguments": fn.get("arguments") or "",
|
||||
},
|
||||
})
|
||||
message["tool_calls"] = tool_calls
|
||||
|
||||
yield {"type": "message", "message": message, "finish_reason": finish_reason}
|
||||
|
||||
Reference in New Issue
Block a user