Coverage for src\funcall\__init__.py: 92%
73 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-22 01:19 +0900
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-22 01:19 +0900
1import dataclasses
2import inspect
3import json
4from collections.abc import Callable
5from logging import getLogger
6from typing import get_type_hints
8from openai.types.responses import (
9 FunctionToolParam,
10 ResponseFunctionToolCall,
11)
13# 新增导入
14from pydantic import BaseModel
15from pydantic.fields import FieldInfo
17logger = getLogger("funcall")
20def param_type(py_type: str | type | FieldInfo | None) -> str:
21 """Map Python types to JSON Schema types"""
22 type_map = {
23 int: "number",
24 float: "number",
25 str: "string",
26 bool: "boolean",
27 list: "array",
28 dict: "object",
29 }
30 origin = getattr(py_type, "__origin__", None)
31 if origin is not None:
32 origin_map = {list: "array", dict: "object"}
33 if origin in origin_map:
34 return origin_map[origin]
35 if py_type in type_map:
36 return type_map[py_type]
37 if BaseModel and isinstance(py_type, type) and issubclass(py_type, BaseModel):
38 return "object"
39 if dataclasses.is_dataclass(py_type):
40 return "object"
41 if isinstance(py_type, FieldInfo):
42 return param_type(py_type.annotation)
43 return "string"
46def generate_meta(func: Callable) -> FunctionToolParam:
47 sig = inspect.signature(func)
48 type_hints = get_type_hints(func)
49 params = {}
50 required = []
51 doc = func.__doc__.strip() if func.__doc__ else ""
53 for name in sig.parameters:
54 hint = type_hints.get(name, str)
55 if isinstance(hint, type) and issubclass(hint, BaseModel):
56 model = hint
57 for field_name, field in model.model_fields.items():
58 desc = field.description if field.description else None
59 params[field_name] = {
60 "type": param_type(field),
61 "description": desc or f"{name}.{field_name}",
62 }
63 if field.is_required():
64 required.append(field_name)
66 elif dataclasses.is_dataclass(hint):
67 # Python dataclass
68 for field in dataclasses.fields(hint):
69 desc = field.metadata.get("description") if "description" in field.metadata else None
70 params[field.name] = {
71 "type": param_type(field.type),
72 "description": desc or f"{name}.{field.name}",
73 }
74 if field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING:
75 required.append(field.name)
76 else:
77 # Normal parameter
78 param_desc = f"The {list(sig.parameters.keys()).index(name) + 1}th parameter"
79 params[name] = {"type": param_type(hint), "description": param_desc}
80 required.append(name)
82 meta: FunctionToolParam = {
83 "type": "function",
84 "name": func.__name__,
85 "description": doc,
86 "parameters": {
87 "type": "object",
88 "properties": params,
89 "required": required,
90 "additionalProperties": False,
91 },
92 "strict": True,
93 }
94 return meta
97class Funcall:
98 def __init__(self, functions: list | None = None) -> None:
99 if functions is None:
100 functions = []
101 self.functions = functions
102 self.function_map = {func.__name__: func for func in functions}
104 def get_tools(self) -> list[FunctionToolParam]:
105 return [generate_meta(func) for func in self.functions]
107 def handle_function_call(self, item: ResponseFunctionToolCall):
108 if item.name in self.function_map:
109 func = self.function_map[item.name]
110 args = item.arguments
111 if BaseModel and issubclass(
112 func.__annotations__.get("data", None),
113 BaseModel,
114 ):
115 model = func.__annotations__["data"]
116 data = model.model_validate_json(args)
117 result = func(data)
118 else:
119 kwargs = json.loads(args)
120 result = func(**kwargs)
122 return result
123 msg = f"Function {item.name} not found"
124 raise ValueError(msg)