mirror of
https://github.com/Zippland/Bubbles.git
synced 2026-02-21 16:16:33 +08:00
refactor
This commit is contained in:
3
function_calls/__init__.py
Normal file
3
function_calls/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Function Call 系统核心模块
|
||||
"""
|
||||
180
function_calls/handlers.py
Normal file
180
function_calls/handlers.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Function Call handlers built on top of structured services."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from commands.context import MessageContext
|
||||
|
||||
from .models import (
|
||||
WeatherArgs,
|
||||
NewsArgs,
|
||||
ReminderArgs,
|
||||
ReminderListArgs,
|
||||
ReminderDeleteArgs,
|
||||
PerplexityArgs,
|
||||
HelpArgs,
|
||||
SummaryArgs,
|
||||
ClearMessagesArgs,
|
||||
InsultArgs,
|
||||
)
|
||||
from .registry import tool_function
|
||||
from .spec import FunctionResult
|
||||
from .services import (
|
||||
build_help_text,
|
||||
build_insult,
|
||||
clear_group_messages,
|
||||
create_reminder,
|
||||
delete_reminder,
|
||||
get_news_digest,
|
||||
get_weather_report,
|
||||
list_reminders,
|
||||
run_perplexity,
|
||||
summarize_messages,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="weather_query",
|
||||
description="查询城市天气预报",
|
||||
examples=["北京天气怎么样", "上海天气", "深圳明天会下雨吗"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_weather(ctx: MessageContext, args: WeatherArgs) -> FunctionResult:
|
||||
result = get_weather_report(args.city)
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
return FunctionResult(handled=True, messages=[result.message], at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="news_query",
|
||||
description="获取今日新闻",
|
||||
examples=["看看今天的新闻", "今日要闻", "新闻"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_news(ctx: MessageContext, args: NewsArgs) -> FunctionResult:
|
||||
result = get_news_digest()
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
return FunctionResult(handled=True, messages=[result.message], at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="reminder_set",
|
||||
description="设置提醒",
|
||||
examples=["提醒我明天下午3点开会", "每天早上8点提醒我吃早餐"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_reminder_set(ctx: MessageContext, args: ReminderArgs) -> FunctionResult:
|
||||
manager = getattr(ctx.robot, 'reminder_manager', None)
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
if not manager:
|
||||
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
|
||||
|
||||
service_result = create_reminder(
|
||||
manager=manager,
|
||||
sender_wxid=ctx.msg.sender,
|
||||
data=args.model_dump(),
|
||||
roomid=ctx.msg.roomid if ctx.is_group else None,
|
||||
)
|
||||
return FunctionResult(handled=True, messages=service_result.messages, at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="reminder_list",
|
||||
description="查看所有提醒",
|
||||
examples=["查看我的提醒", "我有哪些提醒", "提醒列表"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_reminder_list(ctx: MessageContext, args: ReminderListArgs) -> FunctionResult:
|
||||
manager = getattr(ctx.robot, 'reminder_manager', None)
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
if not manager:
|
||||
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
|
||||
|
||||
service_result = list_reminders(manager, ctx.msg.sender, ctx.all_contacts)
|
||||
return FunctionResult(handled=True, messages=service_result.messages, at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="reminder_delete",
|
||||
description="删除提醒",
|
||||
examples=["删除开会的提醒", "取消明天的提醒"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_reminder_delete(ctx: MessageContext, args: ReminderDeleteArgs) -> FunctionResult:
|
||||
manager = getattr(ctx.robot, 'reminder_manager', None)
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
if not manager:
|
||||
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
|
||||
|
||||
service_result = delete_reminder(manager, ctx.msg.sender, args.reminder_id)
|
||||
return FunctionResult(handled=True, messages=service_result.messages, at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="perplexity_search",
|
||||
description="使用Perplexity进行深度搜索查询",
|
||||
examples=["搜索Python最新特性", "查查机器学习教程", "ask什么是量子计算"],
|
||||
scope="both",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_perplexity_search(ctx: MessageContext, args: PerplexityArgs) -> FunctionResult:
|
||||
service_result = run_perplexity(ctx, args.query)
|
||||
if service_result.handled_externally:
|
||||
return FunctionResult(handled=True, messages=[])
|
||||
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
return FunctionResult(handled=True, messages=service_result.messages, at=at if at else "")
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="help",
|
||||
description="显示机器人帮助信息",
|
||||
examples=["help", "帮助", "指令"],
|
||||
scope="both",
|
||||
require_at=False,
|
||||
)
|
||||
def handle_help(ctx: MessageContext, args: HelpArgs) -> FunctionResult:
|
||||
help_text = build_help_text()
|
||||
return FunctionResult(handled=True, messages=[help_text])
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="summary",
|
||||
description="总结群聊最近的消息",
|
||||
examples=["summary", "总结"],
|
||||
scope="group",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_summary(ctx: MessageContext, args: SummaryArgs) -> FunctionResult:
|
||||
result = summarize_messages(ctx)
|
||||
return FunctionResult(handled=True, messages=[result.message])
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="clear_messages",
|
||||
description="清除群聊历史消息记录",
|
||||
examples=["clearmessages", "清除历史"],
|
||||
scope="group",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_clear_messages(ctx: MessageContext, args: ClearMessagesArgs) -> FunctionResult:
|
||||
result = clear_group_messages(ctx)
|
||||
return FunctionResult(handled=True, messages=[result.message])
|
||||
|
||||
|
||||
@tool_function(
|
||||
name="insult",
|
||||
description="骂指定用户(仅限群聊)",
|
||||
examples=["骂一下@某人"],
|
||||
scope="group",
|
||||
require_at=True,
|
||||
)
|
||||
def handle_insult(ctx: MessageContext, args: InsultArgs) -> FunctionResult:
|
||||
result = build_insult(ctx, args.target_user)
|
||||
return FunctionResult(handled=True, messages=[result.message])
|
||||
10
function_calls/init_handlers.py
Normal file
10
function_calls/init_handlers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
初始化所有Function Call处理器
|
||||
导入这个模块会自动注册所有处理器到全局注册表
|
||||
"""
|
||||
from . import handlers # 导入handlers模块会自动执行所有装饰器注册
|
||||
|
||||
# 可以在这里添加一些初始化日志
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Function Call 处理器初始化完成")
|
||||
263
function_calls/llm.py
Normal file
263
function_calls/llm.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""LLM function-call orchestration utilities."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from commands.context import MessageContext
|
||||
|
||||
from .spec import FunctionResult, FunctionSpec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMRunResult:
|
||||
"""Result of the LLM routing pipeline."""
|
||||
|
||||
handled: bool
|
||||
final_response: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class FunctionCallLLM:
|
||||
"""Coordinate function-call capable models with router handlers."""
|
||||
|
||||
def __init__(self, max_function_rounds: int = 5) -> None:
|
||||
self.logger = logger
|
||||
self.max_function_rounds = max_function_rounds
|
||||
|
||||
def run(
|
||||
self,
|
||||
ctx: MessageContext,
|
||||
functions: Dict[str, FunctionSpec],
|
||||
executor: Callable[[FunctionSpec, Dict[str, Any]], FunctionResult],
|
||||
formatter: Callable[[FunctionResult], str],
|
||||
) -> LLMRunResult:
|
||||
"""Execute the function-call loop and return the final assistant response."""
|
||||
if not ctx.text:
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
chat_model = getattr(ctx, "chat", None)
|
||||
if not chat_model and ctx.robot:
|
||||
chat_model = getattr(ctx.robot, "chat", None)
|
||||
|
||||
if not chat_model:
|
||||
self.logger.error("无可用的AI模型")
|
||||
return LLMRunResult(handled=False, error="no_model")
|
||||
|
||||
try:
|
||||
if hasattr(chat_model, "call_with_functions"):
|
||||
return self._run_native_loop(ctx, chat_model, functions, executor, formatter)
|
||||
return self._run_prompt_loop(ctx, chat_model, functions, executor)
|
||||
except Exception as exc: # pragma: no cover - safeguard
|
||||
self.logger.error(f"LLM 调用失败: {exc}")
|
||||
return LLMRunResult(handled=False, error=str(exc))
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Native function-call workflow
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _run_native_loop(
|
||||
self,
|
||||
ctx: MessageContext,
|
||||
chat_model: Any,
|
||||
functions: Dict[str, FunctionSpec],
|
||||
executor: Callable[[FunctionSpec, Dict[str, Any]], FunctionResult],
|
||||
formatter: Callable[[FunctionResult], str],
|
||||
) -> LLMRunResult:
|
||||
openai_functions = self._build_functions_for_openai(functions)
|
||||
messages: List[Dict[str, Any]] = []
|
||||
|
||||
system_prompt = (
|
||||
"You are an assistant that can call tools. "
|
||||
"When you invoke a function, wait for the tool response before replying to the user. "
|
||||
"Only deliver a final answer once you have enough information."
|
||||
)
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": ctx.text})
|
||||
|
||||
for round_index in range(self.max_function_rounds):
|
||||
response = chat_model.call_with_functions(
|
||||
messages=messages,
|
||||
functions=openai_functions,
|
||||
wxid=ctx.get_receiver(),
|
||||
)
|
||||
|
||||
if not getattr(response, "choices", None):
|
||||
self.logger.warning("函数调用返回空响应")
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
message = response.choices[0].message
|
||||
assistant_entry = self._convert_assistant_message(message)
|
||||
messages.append(assistant_entry)
|
||||
|
||||
tool_calls = getattr(message, "tool_calls", None) or []
|
||||
if tool_calls:
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
if function_name not in functions:
|
||||
self.logger.warning(f"模型请求未知函数: {function_name}")
|
||||
tool_content = json.dumps(
|
||||
{
|
||||
"handled": False,
|
||||
"messages": [f"Unknown function: {function_name}"],
|
||||
"metadata": {"error": "unknown_function"},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments or "{}")
|
||||
except json.JSONDecodeError:
|
||||
arguments = {}
|
||||
spec = functions[function_name]
|
||||
result = executor(spec, arguments)
|
||||
tool_content = formatter(result)
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": tool_content,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# 没有工具调用,认为模型给出了最终回答
|
||||
final_content = message.content or ""
|
||||
return LLMRunResult(handled=True, final_response=final_content)
|
||||
|
||||
self.logger.warning("达到最大函数调用轮数,未得到最终回答")
|
||||
return LLMRunResult(handled=False, error="max_rounds")
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Prompt-based fallback workflow
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _run_prompt_loop(
|
||||
self,
|
||||
ctx: MessageContext,
|
||||
chat_model: Any,
|
||||
functions: Dict[str, FunctionSpec],
|
||||
executor: Callable[[FunctionSpec, Dict[str, Any]], FunctionResult],
|
||||
) -> LLMRunResult:
|
||||
system_prompt = self._build_prompt_system_text(functions)
|
||||
user_input = f"用户输入:{ctx.text}"
|
||||
|
||||
ai_response = chat_model.get_answer(
|
||||
user_input,
|
||||
wxid=ctx.get_receiver(),
|
||||
system_prompt_override=system_prompt,
|
||||
)
|
||||
|
||||
json_match = re.search(r"\{.*\}", ai_response, re.DOTALL)
|
||||
if not json_match:
|
||||
self.logger.warning(f"提示词模式下无法解析JSON: {ai_response}")
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
try:
|
||||
decision = json.loads(json_match.group(0))
|
||||
except json.JSONDecodeError as exc:
|
||||
self.logger.error(f"提示词模式 JSON 解析失败: {exc}")
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
action_type = decision.get("action_type")
|
||||
if action_type == "chat":
|
||||
# 提示词模式下无法获得模型最终回答,交给上层兜底
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
if action_type != "function":
|
||||
self.logger.warning(f"未知的action_type: {action_type}")
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
function_name = decision.get("function_name")
|
||||
if function_name not in functions:
|
||||
self.logger.warning(f"未知的功能名 - {function_name}")
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
arguments = decision.get("arguments", {})
|
||||
result = executor(functions[function_name], arguments)
|
||||
if not result.handled:
|
||||
return LLMRunResult(handled=False)
|
||||
|
||||
return LLMRunResult(handled=True, final_response="\n".join(result.messages))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _convert_assistant_message(message: Any) -> Dict[str, Any]:
|
||||
entry: Dict[str, Any] = {
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
}
|
||||
tool_calls = getattr(message, "tool_calls", None)
|
||||
if tool_calls:
|
||||
entry["tool_calls"] = []
|
||||
for tool_call in tool_calls:
|
||||
entry["tool_calls"].append(
|
||||
{
|
||||
"id": tool_call.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call.function.name,
|
||||
"arguments": tool_call.function.arguments,
|
||||
},
|
||||
}
|
||||
)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def _build_functions_for_openai(functions: Dict[str, FunctionSpec]) -> List[Dict[str, Any]]:
|
||||
openai_functions = []
|
||||
for spec in functions.values():
|
||||
openai_functions.append(
|
||||
{
|
||||
"name": spec.name,
|
||||
"description": spec.description,
|
||||
"parameters": spec.parameters_schema,
|
||||
}
|
||||
)
|
||||
return openai_functions
|
||||
|
||||
@staticmethod
|
||||
def _build_prompt_system_text(functions: Dict[str, FunctionSpec]) -> str:
|
||||
prompt = """你是一个智能路由助手。根据用户输入判断是否需要调用以下函数之一。"""
|
||||
for spec in functions.values():
|
||||
prompt += f"\n- {spec.name}: {spec.description}"
|
||||
prompt += """
|
||||
请严格输出JSON:{"action_type": "chat"} 或 {"action_type": "function", "function_name": "...", "arguments": {...}}
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def validate_arguments(self, arguments: Dict[str, Any], schema: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
required_fields = schema.get("required", [])
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
for field in required_fields:
|
||||
if field not in arguments:
|
||||
self.logger.warning(f"缺少必需参数: {field}")
|
||||
return False
|
||||
|
||||
for field, value in arguments.items():
|
||||
if field not in properties:
|
||||
continue
|
||||
expected_type = properties[field].get("type")
|
||||
if expected_type == "string" and not isinstance(value, str):
|
||||
self.logger.warning(f"参数 {field} 类型不正确,期望 string,得到 {type(value)}")
|
||||
return False
|
||||
if expected_type == "integer" and not isinstance(value, int):
|
||||
self.logger.warning(f"参数 {field} 类型不正确,期望 integer,得到 {type(value)}")
|
||||
return False
|
||||
if expected_type == "number" and not isinstance(value, (int, float)):
|
||||
self.logger.warning(f"参数 {field} 类型不正确,期望 number,得到 {type(value)}")
|
||||
return False
|
||||
return True
|
||||
except Exception as exc:
|
||||
self.logger.error(f"参数验证失败: {exc}")
|
||||
return False
|
||||
61
function_calls/models.py
Normal file
61
function_calls/models.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Function Call 参数模型定义
|
||||
"""
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WeatherArgs(BaseModel):
|
||||
"""天气查询参数"""
|
||||
city: str
|
||||
|
||||
|
||||
class NewsArgs(BaseModel):
|
||||
"""新闻查询参数 - 无需参数"""
|
||||
pass
|
||||
|
||||
|
||||
class ReminderArgs(BaseModel):
|
||||
"""设置提醒参数"""
|
||||
|
||||
type: Literal["once", "daily", "weekly"]
|
||||
time: str
|
||||
content: str
|
||||
weekday: Optional[int] = None
|
||||
|
||||
|
||||
class ReminderListArgs(BaseModel):
|
||||
"""查看提醒列表参数 - 无需参数"""
|
||||
pass
|
||||
|
||||
|
||||
class ReminderDeleteArgs(BaseModel):
|
||||
"""删除提醒参数"""
|
||||
|
||||
reminder_id: str
|
||||
|
||||
|
||||
class PerplexityArgs(BaseModel):
|
||||
"""Perplexity搜索参数"""
|
||||
query: str
|
||||
|
||||
|
||||
class HelpArgs(BaseModel):
|
||||
"""帮助信息参数 - 无需参数"""
|
||||
pass
|
||||
|
||||
|
||||
class SummaryArgs(BaseModel):
|
||||
"""消息总结参数 - 无需参数"""
|
||||
pass
|
||||
|
||||
|
||||
class ClearMessagesArgs(BaseModel):
|
||||
"""清除消息参数 - 无需参数"""
|
||||
pass
|
||||
|
||||
|
||||
class InsultArgs(BaseModel):
|
||||
"""骂人功能参数"""
|
||||
target_user: str
|
||||
108
function_calls/registry.py
Normal file
108
function_calls/registry.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
函数注册中心
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, get_type_hints
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .spec import FunctionSpec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 全局函数注册表
|
||||
FUNCTION_REGISTRY: Dict[str, FunctionSpec] = {}
|
||||
|
||||
|
||||
def register_function(spec: FunctionSpec) -> None:
|
||||
"""注册函数到全局注册表"""
|
||||
if spec.name in FUNCTION_REGISTRY:
|
||||
raise ValueError(f"重复的函数名: {spec.name}")
|
||||
FUNCTION_REGISTRY[spec.name] = spec
|
||||
logger.info(f"注册函数: {spec.name} - {spec.description}")
|
||||
|
||||
|
||||
def get_function(name: str) -> FunctionSpec:
|
||||
"""获取指定名称的函数规格"""
|
||||
if name not in FUNCTION_REGISTRY:
|
||||
raise ValueError(f"未找到函数: {name}")
|
||||
return FUNCTION_REGISTRY[name]
|
||||
|
||||
|
||||
def list_functions() -> Dict[str, FunctionSpec]:
|
||||
"""获取所有已注册的函数"""
|
||||
return FUNCTION_REGISTRY.copy()
|
||||
|
||||
|
||||
def build_schema_from_model(model_class) -> Dict[str, Any]:
|
||||
"""从 Pydantic 模型构建 JSON Schema"""
|
||||
if issubclass(model_class, BaseModel):
|
||||
return model_class.model_json_schema()
|
||||
else:
|
||||
# 简单的dataclass或类型注解支持
|
||||
hints = get_type_hints(model_class)
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
for field_name, field_type in hints.items():
|
||||
if field_name.startswith('_'):
|
||||
continue
|
||||
|
||||
properties[field_name] = _type_to_schema(field_type)
|
||||
required.append(field_name)
|
||||
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required
|
||||
}
|
||||
|
||||
|
||||
def _type_to_schema(field_type) -> Dict[str, Any]:
|
||||
"""将Python类型转换为JSON Schema"""
|
||||
if field_type == str:
|
||||
return {"type": "string"}
|
||||
elif field_type == int:
|
||||
return {"type": "integer"}
|
||||
elif field_type == float:
|
||||
return {"type": "number"}
|
||||
elif field_type == bool:
|
||||
return {"type": "boolean"}
|
||||
else:
|
||||
return {"type": "string", "description": f"类型: {field_type}"}
|
||||
|
||||
|
||||
def tool_function(name: str, description: str, examples: list[str] = None, **meta):
|
||||
"""
|
||||
装饰器:自动注册函数到Function Call系统
|
||||
|
||||
@tool_function(
|
||||
name="weather_query",
|
||||
description="查询天气",
|
||||
examples=["北京天气怎么样"]
|
||||
)
|
||||
def handle_weather(ctx: MessageContext, args: WeatherArgs) -> FunctionResult:
|
||||
pass
|
||||
"""
|
||||
def wrapper(func):
|
||||
# 获取函数参数类型注解
|
||||
hints = get_type_hints(func)
|
||||
args_type = hints.get('args')
|
||||
|
||||
if args_type:
|
||||
schema = build_schema_from_model(args_type)
|
||||
else:
|
||||
schema = {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
spec = FunctionSpec(
|
||||
name=name,
|
||||
description=description,
|
||||
parameters_schema=schema,
|
||||
handler=func,
|
||||
examples=examples or [],
|
||||
**meta
|
||||
)
|
||||
|
||||
register_function(spec)
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
227
function_calls/router.py
Normal file
227
function_calls/router.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""Function Call 路由器"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from commands.context import MessageContext
|
||||
|
||||
from .spec import FunctionResult, FunctionSpec
|
||||
from .registry import list_functions
|
||||
from .llm import FunctionCallLLM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FunctionCallRouter:
|
||||
"""函数调用路由器"""
|
||||
|
||||
def __init__(self, robot_instance=None):
|
||||
self.robot_instance = robot_instance
|
||||
self.llm = FunctionCallLLM()
|
||||
self.logger = logger
|
||||
|
||||
def _check_scope_and_permissions(self, ctx: MessageContext, spec: FunctionSpec) -> bool:
|
||||
"""检查作用域和权限"""
|
||||
# 1. 检查作用域
|
||||
if spec.scope != "both":
|
||||
if (spec.scope == "group" and not ctx.is_group) or \
|
||||
(spec.scope == "private" and ctx.is_group):
|
||||
return False
|
||||
|
||||
# 2. 检查是否需要@机器人(仅在群聊中有效)
|
||||
if ctx.is_group and spec.require_at and not ctx.is_at_bot:
|
||||
return False
|
||||
|
||||
# 3. 检查权限(如果有auth字段)
|
||||
if spec.auth:
|
||||
# TODO: 实现权限检查逻辑
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def _try_direct_command_match(self, ctx: MessageContext) -> Optional[str]:
|
||||
"""
|
||||
尝试直接命令匹配,避免不必要的LLM调用
|
||||
|
||||
返回匹配的函数名,如果没有匹配则返回None
|
||||
"""
|
||||
text = ctx.text.strip().lower()
|
||||
|
||||
# 定义一些明确的命令关键字映射
|
||||
direct_commands = {
|
||||
"help": "help",
|
||||
"帮助": "help",
|
||||
"指令": "help",
|
||||
"新闻": "news_query",
|
||||
"summary": "summary",
|
||||
"总结": "summary",
|
||||
"clearmessages": "clear_messages",
|
||||
"清除历史": "clear_messages"
|
||||
}
|
||||
|
||||
# 检查完全匹配
|
||||
if text in direct_commands:
|
||||
return direct_commands[text]
|
||||
|
||||
# 检查以特定前缀开头的命令
|
||||
if text.startswith("ask ") and len(text) > 4:
|
||||
return "perplexity_search"
|
||||
|
||||
if text.startswith("天气") or text.startswith("天气预报"):
|
||||
return "weather_query"
|
||||
|
||||
if text in ["查看提醒", "我的提醒", "提醒列表"]:
|
||||
return "reminder_list"
|
||||
|
||||
if text.startswith("骂一下"):
|
||||
return "insult"
|
||||
|
||||
return None
|
||||
|
||||
def dispatch(self, ctx: MessageContext) -> bool:
|
||||
"""
|
||||
分发消息到函数处理器
|
||||
|
||||
返回: 是否成功处理
|
||||
"""
|
||||
try:
|
||||
# 确保context可以访问到robot实例
|
||||
if self.robot_instance and not ctx.robot:
|
||||
ctx.robot = self.robot_instance
|
||||
if hasattr(self.robot_instance, 'LOG') and not ctx.logger:
|
||||
ctx.logger = self.robot_instance.LOG
|
||||
|
||||
if ctx.logger:
|
||||
ctx.logger.debug(f"FunctionCallRouter 开始处理: '{ctx.text}', 来自: {ctx.sender_name}")
|
||||
|
||||
# 获取所有可用函数
|
||||
functions = list_functions()
|
||||
if not functions:
|
||||
self.logger.warning("没有注册任何函数")
|
||||
return False
|
||||
|
||||
# 第一步:尝试直接命令匹配
|
||||
direct_function = self._try_direct_command_match(ctx)
|
||||
if direct_function and direct_function in functions:
|
||||
spec = functions[direct_function]
|
||||
|
||||
if not self._check_scope_and_permissions(ctx, spec):
|
||||
return False
|
||||
|
||||
arguments = self._extract_arguments_for_direct_command(ctx, direct_function)
|
||||
if not self.llm.validate_arguments(arguments, spec.parameters_schema):
|
||||
self.logger.warning(f"直接命令 {direct_function} 参数验证失败")
|
||||
return False
|
||||
|
||||
result = self._invoke_function(ctx, spec, arguments)
|
||||
if result.handled:
|
||||
result.dispatch(ctx)
|
||||
return True
|
||||
# 如果没有处理成功,继续尝试LLM流程
|
||||
|
||||
# 第二步:使用LLM执行多轮函数调用
|
||||
llm_result = self.llm.run(
|
||||
ctx,
|
||||
functions,
|
||||
lambda spec, args: self._invoke_function(ctx, spec, args),
|
||||
self._format_tool_response,
|
||||
)
|
||||
|
||||
if not llm_result.handled:
|
||||
return False
|
||||
|
||||
if llm_result.final_response:
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
ctx.send_text(llm_result.final_response, at)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FunctionCallRouter dispatch 异常: {e}")
|
||||
return False
|
||||
|
||||
def _extract_arguments_for_direct_command(self, ctx: MessageContext, function_name: str) -> Dict[str, Any]:
|
||||
"""为直接命令提取参数"""
|
||||
text = ctx.text.strip()
|
||||
|
||||
if function_name == "weather_query":
|
||||
# 提取城市名
|
||||
if text.startswith("天气预报 "):
|
||||
city = text[4:].strip()
|
||||
elif text.startswith("天气 "):
|
||||
city = text[3:].strip()
|
||||
else:
|
||||
city = ""
|
||||
return {"city": city}
|
||||
|
||||
elif function_name == "perplexity_search":
|
||||
# 提取搜索查询
|
||||
if text.startswith("ask "):
|
||||
query = text[4:].strip()
|
||||
else:
|
||||
query = text
|
||||
return {"query": query}
|
||||
|
||||
elif function_name == "insult":
|
||||
# 提取要骂的用户
|
||||
import re
|
||||
match = re.search(r"骂一下\s*@([^\s@]+)", text)
|
||||
target_user = match.group(1) if match else ""
|
||||
return {"target_user": target_user}
|
||||
|
||||
# 对于不需要参数的函数,返回空字典
|
||||
return {}
|
||||
|
||||
def _invoke_function(self, ctx: MessageContext, spec: FunctionSpec, arguments: Dict[str, Any]) -> FunctionResult:
|
||||
"""调用函数处理器,返回结构化结果"""
|
||||
try:
|
||||
if ctx.logger:
|
||||
ctx.logger.info(f"执行函数: {spec.name}, 参数: {arguments}")
|
||||
|
||||
args_instance = self._create_args_instance(spec, arguments)
|
||||
result = spec.handler(ctx, args_instance)
|
||||
|
||||
if not isinstance(result, FunctionResult):
|
||||
raise TypeError(f"函数 {spec.name} 返回了非 FunctionResult 类型: {type(result)}")
|
||||
|
||||
if ctx.logger and not result.handled:
|
||||
ctx.logger.warning(f"函数 {spec.name} 返回未处理状态")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
self.logger.error(f"执行函数 {spec.name} 异常: {exc}")
|
||||
return FunctionResult(
|
||||
handled=False,
|
||||
messages=[f"函数 {spec.name} 执行失败: {exc}"],
|
||||
metadata={"error": str(exc)},
|
||||
)
|
||||
|
||||
def _create_args_instance(self, spec: FunctionSpec, arguments: Dict[str, Any]):
|
||||
"""根据函数规格创建参数实例"""
|
||||
try:
|
||||
# 获取函数的类型注解
|
||||
from typing import get_type_hints
|
||||
hints = get_type_hints(spec.handler)
|
||||
args_type = hints.get('args')
|
||||
|
||||
if args_type:
|
||||
# 如果是Pydantic模型
|
||||
if hasattr(args_type, 'model_validate'):
|
||||
return args_type.model_validate(arguments)
|
||||
elif hasattr(args_type, '__init__'):
|
||||
# 普通类
|
||||
return args_type(**arguments)
|
||||
|
||||
# 如果没有类型注解,返回参数字典
|
||||
return arguments
|
||||
|
||||
except Exception as exc:
|
||||
self.logger.error(f"创建参数实例失败: {exc}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _format_tool_response(result: FunctionResult) -> str:
|
||||
"""将 FunctionResult 格式化为供 LLM 读取的 tool 响应"""
|
||||
return result.to_tool_content()
|
||||
22
function_calls/services/__init__.py
Normal file
22
function_calls/services/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Service helpers for Function Call handlers."""
|
||||
|
||||
from .weather import get_weather_report
|
||||
from .news import get_news_digest
|
||||
from .reminder import create_reminder, list_reminders, delete_reminder
|
||||
from .help import build_help_text
|
||||
from .group_tools import summarize_messages, clear_group_messages
|
||||
from .perplexity import run_perplexity
|
||||
from .insult import build_insult
|
||||
|
||||
__all__ = [
|
||||
"get_weather_report",
|
||||
"get_news_digest",
|
||||
"create_reminder",
|
||||
"list_reminders",
|
||||
"delete_reminder",
|
||||
"build_help_text",
|
||||
"summarize_messages",
|
||||
"clear_group_messages",
|
||||
"run_perplexity",
|
||||
"build_insult",
|
||||
]
|
||||
47
function_calls/services/group_tools.py
Normal file
47
function_calls/services/group_tools.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Group related utilities for Function Call handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from commands.context import MessageContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupToolResult:
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
def summarize_messages(ctx: MessageContext) -> GroupToolResult:
|
||||
if not ctx.is_group:
|
||||
return GroupToolResult(success=True, message="⚠️ 消息总结功能仅支持群聊")
|
||||
|
||||
if not ctx.robot or not hasattr(ctx.robot, "message_summary") or not hasattr(ctx.robot, "chat"):
|
||||
return GroupToolResult(success=False, message="⚠️ 消息总结功能不可用")
|
||||
|
||||
try:
|
||||
summary = ctx.robot.message_summary.summarize_messages(ctx.msg.roomid, ctx.robot.chat)
|
||||
return GroupToolResult(success=True, message=summary)
|
||||
except Exception as exc:
|
||||
if ctx.logger:
|
||||
ctx.logger.error(f"生成消息总结出错: {exc}")
|
||||
return GroupToolResult(success=False, message="⚠️ 生成消息总结失败")
|
||||
|
||||
|
||||
def clear_group_messages(ctx: MessageContext) -> GroupToolResult:
|
||||
if not ctx.is_group:
|
||||
return GroupToolResult(success=True, message="⚠️ 消息历史管理功能仅支持群聊")
|
||||
|
||||
if not ctx.robot or not hasattr(ctx.robot, "message_summary"):
|
||||
return GroupToolResult(success=False, message="⚠️ 消息历史管理功能不可用")
|
||||
|
||||
try:
|
||||
cleared = ctx.robot.message_summary.clear_message_history(ctx.msg.roomid)
|
||||
if cleared:
|
||||
return GroupToolResult(success=True, message="✅ 已清除本群的消息历史记录")
|
||||
return GroupToolResult(success=True, message="⚠️ 本群没有消息历史记录")
|
||||
except Exception as exc:
|
||||
if ctx.logger:
|
||||
ctx.logger.error(f"清除消息历史出错: {exc}")
|
||||
return GroupToolResult(success=False, message="⚠️ 清除消息历史失败")
|
||||
34
function_calls/services/help.py
Normal file
34
function_calls/services/help.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Static help text utility."""
|
||||
from __future__ import annotations
|
||||
|
||||
HELP_LINES = [
|
||||
"🤖 泡泡的指令列表 🤖",
|
||||
"",
|
||||
"【实用工具】",
|
||||
"- 天气/温度 [城市名]",
|
||||
"- 天气预报/预报 [城市名]",
|
||||
"- 新闻",
|
||||
"- ask [问题]",
|
||||
"",
|
||||
"【决斗 & 偷袭】",
|
||||
"- 决斗@XX",
|
||||
"- 偷袭@XX",
|
||||
"- 决斗排行/排行榜",
|
||||
"- 我的战绩/决斗战绩",
|
||||
"- 我的装备/查看装备",
|
||||
"- 改名 [旧名] [新名]",
|
||||
"",
|
||||
"【提醒】",
|
||||
"- 提醒xxxxx:一次性、每日、每周",
|
||||
"- 查看提醒/我的提醒/提醒列表",
|
||||
"- 删..提醒..",
|
||||
"",
|
||||
"【群聊工具】",
|
||||
"- summary/总结",
|
||||
"- clearmessages/清除历史",
|
||||
]
|
||||
|
||||
|
||||
def build_help_text() -> str:
|
||||
"""Return formatted help text."""
|
||||
return "\n".join(HELP_LINES)
|
||||
50
function_calls/services/insult.py
Normal file
50
function_calls/services/insult.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Group insult helper utilities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from commands.context import MessageContext
|
||||
from function.func_insult import generate_random_insult
|
||||
|
||||
|
||||
@dataclass
|
||||
class InsultResult:
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
def build_insult(ctx: MessageContext, target_name: str) -> InsultResult:
|
||||
if not ctx.is_group:
|
||||
return InsultResult(success=True, message="❌ 骂人功能只支持群聊哦~")
|
||||
|
||||
cleaned = target_name.strip()
|
||||
if not cleaned:
|
||||
return InsultResult(success=False, message="❌ 需要提供要骂的对象")
|
||||
|
||||
actual_target = cleaned
|
||||
target_wxid: Optional[str] = None
|
||||
|
||||
try:
|
||||
members = ctx.room_members
|
||||
if members:
|
||||
for wxid, name in members.items():
|
||||
if cleaned == name:
|
||||
target_wxid = wxid
|
||||
actual_target = name
|
||||
break
|
||||
if target_wxid is None:
|
||||
for wxid, name in members.items():
|
||||
if cleaned in name and wxid != ctx.robot_wxid:
|
||||
target_wxid = wxid
|
||||
actual_target = name
|
||||
break
|
||||
except Exception as exc:
|
||||
if ctx.logger:
|
||||
ctx.logger.error(f"查找群成员信息时出错: {exc}")
|
||||
|
||||
if target_wxid and target_wxid == ctx.robot_wxid:
|
||||
return InsultResult(success=True, message="😅 不行,我不能骂我自己。")
|
||||
|
||||
insult_text = generate_random_insult(actual_target)
|
||||
return InsultResult(success=True, message=insult_text)
|
||||
36
function_calls/services/news.py
Normal file
36
function_calls/services/news.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""News related service helpers for Function Call handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from function.func_news import News
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsResult:
|
||||
success: bool
|
||||
message: str
|
||||
is_today: Optional[bool] = None
|
||||
|
||||
|
||||
def get_news_digest() -> NewsResult:
|
||||
"""Fetch latest news digest."""
|
||||
try:
|
||||
news_instance = News()
|
||||
is_today, content = news_instance.get_important_news()
|
||||
if is_today:
|
||||
message = f"📰 今日要闻来啦:\n{content}"
|
||||
else:
|
||||
if content:
|
||||
message = f"ℹ️ 今日新闻暂未发布,为您找到最近的一条新闻:\n{content}"
|
||||
else:
|
||||
message = "❌ 获取新闻失败,请稍后重试"
|
||||
return NewsResult(success=True, message=message, is_today=is_today)
|
||||
except Exception as exc:
|
||||
logger.error(f"获取新闻失败: {exc}")
|
||||
return NewsResult(success=False, message="❌ 获取新闻时发生错误")
|
||||
62
function_calls/services/perplexity.py
Normal file
62
function_calls/services/perplexity.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Perplexity integration helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from commands.context import MessageContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerplexityResult:
|
||||
success: bool
|
||||
messages: List[str]
|
||||
handled_externally: bool = False
|
||||
|
||||
|
||||
def run_perplexity(ctx: MessageContext, query: str) -> PerplexityResult:
|
||||
query = query.strip()
|
||||
if not query:
|
||||
at = ctx.msg.sender if ctx.is_group else ""
|
||||
return PerplexityResult(success=True, messages=["请告诉我你想搜索什么内容"], handled_externally=False)
|
||||
|
||||
perplexity_instance = getattr(ctx.robot, 'perplexity', None)
|
||||
if not perplexity_instance:
|
||||
return PerplexityResult(success=True, messages=["❌ Perplexity搜索功能当前不可用"], handled_externally=False)
|
||||
|
||||
content_for_perplexity = f"ask {query}"
|
||||
chat_id = ctx.get_receiver()
|
||||
sender_wxid = ctx.msg.sender
|
||||
room_id = ctx.msg.roomid if ctx.is_group else None
|
||||
|
||||
was_handled, fallback_prompt = perplexity_instance.process_message(
|
||||
content=content_for_perplexity,
|
||||
chat_id=chat_id,
|
||||
sender=sender_wxid,
|
||||
roomid=room_id,
|
||||
from_group=ctx.is_group,
|
||||
send_text_func=ctx.send_text
|
||||
)
|
||||
|
||||
if was_handled:
|
||||
return PerplexityResult(success=True, messages=[], handled_externally=True)
|
||||
|
||||
if fallback_prompt:
|
||||
chat_model = getattr(ctx, 'chat', None) or (getattr(ctx.robot, 'chat', None) if ctx.robot else None)
|
||||
if chat_model:
|
||||
try:
|
||||
import time
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
formatted_question = f"[{current_time}] {ctx.sender_name}: {query}"
|
||||
answer = chat_model.get_answer(
|
||||
question=formatted_question,
|
||||
wxid=ctx.get_receiver(),
|
||||
system_prompt_override=fallback_prompt
|
||||
)
|
||||
if answer:
|
||||
return PerplexityResult(success=True, messages=[answer], handled_externally=False)
|
||||
except Exception as exc:
|
||||
if ctx.logger:
|
||||
ctx.logger.error(f"默认AI处理失败: {exc}")
|
||||
|
||||
return PerplexityResult(success=True, messages=["❌ Perplexity搜索时发生错误"], handled_externally=False)
|
||||
101
function_calls/services/reminder.py
Normal file
101
function_calls/services/reminder.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Reminder related services for Function Call handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from function.func_reminder import ReminderManager
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReminderServiceResult:
|
||||
success: bool
|
||||
messages: List[str]
|
||||
|
||||
|
||||
_WEEKDAY_LABELS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
_TYPE_LABELS = {"once": "一次性", "daily": "每日", "weekly": "每周"}
|
||||
|
||||
|
||||
def _format_schedule(data: Dict[str, Any]) -> str:
|
||||
reminder_type = data.get("type", "once")
|
||||
time_str = data.get("time", "?")
|
||||
|
||||
if reminder_type == "once":
|
||||
return f"{time_str} (一次性)"
|
||||
if reminder_type == "daily":
|
||||
return f"每天 {time_str}"
|
||||
if reminder_type == "weekly":
|
||||
weekday = data.get("weekday")
|
||||
if isinstance(weekday, int) and 0 <= weekday < len(_WEEKDAY_LABELS):
|
||||
return f"每周{_WEEKDAY_LABELS[weekday]} {time_str}"
|
||||
return f"每周 {time_str}"
|
||||
return f"{time_str}"
|
||||
|
||||
|
||||
def create_reminder(
|
||||
manager: ReminderManager,
|
||||
sender_wxid: str,
|
||||
data: Dict[str, Any],
|
||||
roomid: Optional[str]
|
||||
) -> ReminderServiceResult:
|
||||
payload = {
|
||||
"type": data["type"],
|
||||
"time": data["time"],
|
||||
"content": data["content"],
|
||||
}
|
||||
if data.get("weekday") is not None:
|
||||
payload["weekday"] = data["weekday"]
|
||||
|
||||
success, info = manager.add_reminder(sender_wxid, payload, roomid=roomid)
|
||||
if not success:
|
||||
return ReminderServiceResult(success=False, messages=[f"❌ 设置提醒失败:{info}"])
|
||||
|
||||
schedule = payload.copy()
|
||||
message = (
|
||||
"✅ 已为您设置{type_label}提醒\n"
|
||||
"时间: {schedule}\n"
|
||||
"内容: {content}"
|
||||
).format(
|
||||
type_label=_TYPE_LABELS.get(payload["type"], ""),
|
||||
schedule=_format_schedule(payload),
|
||||
content=payload["content"],
|
||||
)
|
||||
return ReminderServiceResult(success=True, messages=[message])
|
||||
|
||||
|
||||
def list_reminders(
|
||||
manager: ReminderManager,
|
||||
sender_wxid: str,
|
||||
contacts: Dict[str, str]
|
||||
) -> ReminderServiceResult:
|
||||
reminders = manager.list_reminders(sender_wxid)
|
||||
if not reminders:
|
||||
return ReminderServiceResult(success=True, messages=["您还没有设置任何提醒。"])
|
||||
|
||||
lines: List[str] = ["📝 您设置的提醒列表(包括私聊和群聊):"]
|
||||
for idx, reminder in enumerate(reminders, start=1):
|
||||
schedule_display = _format_schedule({
|
||||
"type": reminder.get("type"),
|
||||
"time": reminder.get("time_str"),
|
||||
"weekday": reminder.get("weekday"),
|
||||
})
|
||||
if reminder.get("type") == "once":
|
||||
schedule_display = reminder.get("time_str", "?")
|
||||
scope = "[私聊]"
|
||||
roomid = reminder.get("roomid")
|
||||
if roomid:
|
||||
room_name = contacts.get(roomid) or roomid[:8]
|
||||
scope = f"[群:{room_name}]"
|
||||
lines.append(
|
||||
f"{idx}. [ID: {reminder.get('id', '')[:6]}] {scope}{schedule_display}: {reminder.get('content', '')}"
|
||||
)
|
||||
|
||||
return ReminderServiceResult(success=True, messages=["\n".join(lines)])
|
||||
|
||||
|
||||
def delete_reminder(manager: ReminderManager, sender_wxid: str, reminder_id: str) -> ReminderServiceResult:
|
||||
success, info = manager.delete_reminder(sender_wxid, reminder_id)
|
||||
if success:
|
||||
return ReminderServiceResult(success=True, messages=[f"✅ {info}"])
|
||||
return ReminderServiceResult(success=False, messages=[f"❌ {info}"])
|
||||
65
function_calls/services/weather.py
Normal file
65
function_calls/services/weather.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Weather related service helpers for Function Call handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from function.func_weather import Weather
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherResult:
|
||||
success: bool
|
||||
message: str
|
||||
city: Optional[str] = None
|
||||
|
||||
|
||||
def _load_city_codes() -> dict[str, str]:
|
||||
"""Load mapping between city names and weather codes."""
|
||||
city_code_path = os.path.join(os.path.dirname(__file__), '..', '..', 'function', 'main_city.json')
|
||||
with open(city_code_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_weather_report(city_name: str) -> WeatherResult:
|
||||
"""Return a weather report for a given city.
|
||||
|
||||
Args:
|
||||
city_name: City provided by the user.
|
||||
|
||||
Returns:
|
||||
WeatherResult containing success status and message.
|
||||
"""
|
||||
city = city_name.strip()
|
||||
if not city:
|
||||
return WeatherResult(success=False, message="🤔 请告诉我你想查询哪个城市的天气")
|
||||
|
||||
try:
|
||||
city_codes = _load_city_codes()
|
||||
except Exception as exc: # pragma: no cover - IO failure is rare
|
||||
logger.error(f"加载城市代码失败: {exc}")
|
||||
return WeatherResult(success=False, message="⚠️ 抱歉,天气功能暂时不可用")
|
||||
|
||||
code = city_codes.get(city)
|
||||
if not code:
|
||||
for name, value in city_codes.items():
|
||||
if city in name:
|
||||
code = value
|
||||
city = name
|
||||
break
|
||||
|
||||
if not code:
|
||||
return WeatherResult(success=False, message=f"😕 找不到城市 '{city_name}' 的天气信息")
|
||||
|
||||
try:
|
||||
weather_text = Weather(code).get_weather(include_forecast=True)
|
||||
return WeatherResult(success=True, message=weather_text, city=city)
|
||||
except Exception as exc: # pragma: no cover - upstream API failure
|
||||
logger.error(f"获取天气数据失败: {exc}")
|
||||
return WeatherResult(success=False, message=f"😥 获取 {city} 天气时遇到问题")
|
||||
50
function_calls/spec.py
Normal file
50
function_calls/spec.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""函数规格定义与相关数据结构"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from commands.context import MessageContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionResult:
|
||||
"""Standardized execution result returned by handlers."""
|
||||
|
||||
handled: bool
|
||||
messages: list[str] = field(default_factory=list)
|
||||
at: str = ""
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def dispatch(self, ctx: MessageContext) -> None:
|
||||
"""Send messages through the context when handled successfully."""
|
||||
if not self.handled:
|
||||
return
|
||||
|
||||
for message in self.messages:
|
||||
ctx.send_text(message, self.at)
|
||||
|
||||
def to_tool_content(self) -> str:
|
||||
"""Serialize result for LLM tool messages."""
|
||||
payload = {
|
||||
"handled": self.handled,
|
||||
"messages": self.messages,
|
||||
"metadata": self.metadata or {},
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionSpec:
|
||||
"""函数规格定义"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
parameters_schema: Dict[str, Any]
|
||||
handler: Callable[[MessageContext, Any], FunctionResult]
|
||||
examples: list[str] = field(default_factory=list)
|
||||
scope: str = "both" # group / private / both
|
||||
require_at: bool = False
|
||||
auth: Optional[str] = None # 权限标签(可选)
|
||||
Reference in New Issue
Block a user