codegnipy 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codegnipy/tools.py ADDED
@@ -0,0 +1,481 @@
1
+ """
2
+ Codegnipy 工具调用模块
3
+
4
+ 提供 Function Calling / Tool Use 支持,让 LLM 能够调用外部函数。
5
+ """
6
+
7
+ import json
8
+ import inspect
9
+ from dataclasses import dataclass
10
+ from typing import (
11
+ Any, Callable, Dict, List, Optional, Type, Union,
12
+ get_type_hints, get_origin, get_args
13
+ )
14
+ from enum import Enum
15
+ from functools import wraps
16
+
17
+ from .runtime import LLMConfig, CognitiveContext
18
+
19
+
20
+ class ToolType(Enum):
21
+ """工具类型"""
22
+ FUNCTION = "function"
23
+
24
+
25
+ @dataclass
26
+ class ToolParameter:
27
+ """工具参数定义"""
28
+ name: str
29
+ param_type: str
30
+ description: str = ""
31
+ required: bool = True
32
+ enum: Optional[List[str]] = None
33
+ default: Any = None
34
+
35
+ def to_json_schema(self) -> dict:
36
+ """转换为 JSON Schema"""
37
+ schema: Dict[str, Any] = {
38
+ "type": self.param_type,
39
+ "description": self.description
40
+ }
41
+ if self.enum:
42
+ schema["enum"] = self.enum
43
+ return schema
44
+
45
+
46
+ @dataclass
47
+ class ToolDefinition:
48
+ """工具定义"""
49
+ name: str
50
+ description: str
51
+ parameters: List[ToolParameter]
52
+ type: ToolType = ToolType.FUNCTION
53
+ handler: Optional[Callable] = None
54
+
55
+ def to_openai_tool(self) -> dict:
56
+ """转换为 OpenAI 工具格式"""
57
+ properties = {}
58
+ required = []
59
+
60
+ for param in self.parameters:
61
+ properties[param.name] = param.to_json_schema()
62
+ if param.required:
63
+ required.append(param.name)
64
+
65
+ return {
66
+ "type": self.type.value,
67
+ "function": {
68
+ "name": self.name,
69
+ "description": self.description,
70
+ "parameters": {
71
+ "type": "object",
72
+ "properties": properties,
73
+ "required": required
74
+ }
75
+ }
76
+ }
77
+
78
+
79
+ @dataclass
80
+ class ToolCall:
81
+ """工具调用请求"""
82
+ id: str
83
+ name: str
84
+ arguments: dict
85
+
86
+ def execute(self, handler: Callable) -> Any:
87
+ """执行工具调用"""
88
+ return handler(**self.arguments)
89
+
90
+
91
+ @dataclass
92
+ class ToolResult:
93
+ """工具调用结果"""
94
+ tool_call_id: str
95
+ name: str
96
+ arguments: dict
97
+ result: Any
98
+ error: Optional[str] = None
99
+
100
+ def to_openai_format(self) -> dict:
101
+ """转换为 OpenAI 格式"""
102
+ content = json.dumps(self.result) if self.result else self.error
103
+ return {
104
+ "role": "tool",
105
+ "tool_call_id": self.tool_call_id,
106
+ "name": self.name,
107
+ "content": content
108
+ }
109
+
110
+
111
+ class ToolRegistry:
112
+ """工具注册表"""
113
+
114
+ def __init__(self):
115
+ self._tools: Dict[str, ToolDefinition] = {}
116
+
117
+ def register(
118
+ self,
119
+ func: Optional[Callable] = None,
120
+ *,
121
+ name: Optional[str] = None,
122
+ description: str = ""
123
+ ) -> Callable:
124
+ """
125
+ 装饰器:注册工具函数
126
+
127
+ 支持 @registry.register 和 @registry.register() 两种用法。
128
+
129
+ 示例:
130
+ @registry.register
131
+ def get_weather(city: str) -> str:
132
+ return f"{city}的天气晴朗"
133
+
134
+ @registry.register(description="获取当前天气")
135
+ def get_weather2(city: str) -> str:
136
+ return f"{city}的天气晴朗"
137
+ """
138
+ def decorator(fn: Callable) -> Callable:
139
+ tool_name = name or fn.__name__
140
+ tool_desc = description or fn.__doc__ or f"执行 {tool_name}"
141
+
142
+ # 从函数签名推断参数
143
+ sig = inspect.signature(fn)
144
+ hints = get_type_hints(fn) if hasattr(fn, '__annotations__') else {}
145
+
146
+ parameters = []
147
+ for param_name, param in sig.parameters.items():
148
+ if param_name == 'self':
149
+ continue
150
+
151
+ param_type = self._python_type_to_json(hints.get(param_name, str))
152
+ required = param.default is inspect.Parameter.empty
153
+ default = None if required else param.default
154
+
155
+ parameters.append(ToolParameter(
156
+ name=param_name,
157
+ param_type=param_type,
158
+ description=f"参数 {param_name}",
159
+ required=required,
160
+ default=default
161
+ ))
162
+
163
+ tool = ToolDefinition(
164
+ name=tool_name,
165
+ description=tool_desc,
166
+ parameters=parameters,
167
+ handler=fn
168
+ )
169
+
170
+ self._tools[tool_name] = tool
171
+
172
+ @wraps(fn)
173
+ def wrapper(*args, **kwargs):
174
+ return fn(*args, **kwargs)
175
+
176
+ wrapper._tool_definition = tool # type: ignore[attr-defined]
177
+
178
+ return wrapper
179
+
180
+ # 支持 @registry.register 和 @registry.register() 两种用法
181
+ if func is not None:
182
+ return decorator(func)
183
+ return decorator
184
+
185
+ def add_tool(self, tool: ToolDefinition) -> None:
186
+ """添加工具定义"""
187
+ self._tools[tool.name] = tool
188
+
189
+ def get_tool(self, name: str) -> Optional[ToolDefinition]:
190
+ """获取工具定义"""
191
+ return self._tools.get(name)
192
+
193
+ def get_all_tools(self) -> List[ToolDefinition]:
194
+ """获取所有工具"""
195
+ return list(self._tools.values())
196
+
197
+ def get_openai_tools(self) -> List[dict]:
198
+ """获取 OpenAI 格式的工具列表"""
199
+ return [tool.to_openai_tool() for tool in self._tools.values()]
200
+
201
+ def execute(self, tool_call: ToolCall) -> ToolResult:
202
+ """执行工具调用"""
203
+ tool = self.get_tool(tool_call.name)
204
+
205
+ if tool is None:
206
+ return ToolResult(
207
+ tool_call_id=tool_call.id,
208
+ name=tool_call.name,
209
+ arguments=tool_call.arguments,
210
+ result=None,
211
+ error=f"Unknown tool: {tool_call.name}"
212
+ )
213
+
214
+ if tool.handler is None:
215
+ return ToolResult(
216
+ tool_call_id=tool_call.id,
217
+ name=tool_call.name,
218
+ arguments=tool_call.arguments,
219
+ result=None,
220
+ error=f"No handler for tool: {tool_call.name}"
221
+ )
222
+
223
+ try:
224
+ result = tool.handler(**tool_call.arguments)
225
+ return ToolResult(
226
+ tool_call_id=tool_call.id,
227
+ name=tool_call.name,
228
+ arguments=tool_call.arguments,
229
+ result=result
230
+ )
231
+ except Exception as e:
232
+ return ToolResult(
233
+ tool_call_id=tool_call.id,
234
+ name=tool_call.name,
235
+ arguments=tool_call.arguments,
236
+ result=None,
237
+ error=str(e)
238
+ )
239
+
240
+ @staticmethod
241
+ def _python_type_to_json(python_type: Type) -> str:
242
+ """将 Python 类型转换为 JSON Schema 类型"""
243
+ type_map = {
244
+ str: "string",
245
+ int: "integer",
246
+ float: "number",
247
+ bool: "boolean",
248
+ list: "array",
249
+ dict: "object",
250
+ }
251
+
252
+ # 处理 Optional 类型
253
+ origin = get_origin(python_type)
254
+ if origin is Union:
255
+ args = get_args(python_type)
256
+ # Optional[X] 实际上是 Union[X, None]
257
+ non_none_args = [a for a in args if a is not type(None)]
258
+ if non_none_args:
259
+ return ToolRegistry._python_type_to_json(non_none_args[0])
260
+
261
+ if origin is list:
262
+ return "array"
263
+ if origin is dict:
264
+ return "object"
265
+
266
+ return type_map.get(python_type, "string")
267
+
268
+
269
+ def tool(
270
+ name: Optional[str] = None,
271
+ description: str = ""
272
+ ) -> Callable:
273
+ """
274
+ 装饰器:定义工具函数
275
+
276
+ 示例:
277
+ @tool(description="获取指定城市的天气信息")
278
+ def get_weather(city: str, unit: str = "celsius") -> str:
279
+ return f"{city}当前温度25{unit}"
280
+ """
281
+ def decorator(func: Callable) -> Callable:
282
+ tool_name = name or func.__name__
283
+ tool_desc = description or func.__doc__ or f"执行 {tool_name}"
284
+
285
+ sig = inspect.signature(func)
286
+ hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
287
+
288
+ parameters = []
289
+ for param_name, param in sig.parameters.items():
290
+ if param_name == 'self':
291
+ continue
292
+
293
+ param_type = ToolRegistry._python_type_to_json(hints.get(param_name, str))
294
+ required = param.default is inspect.Parameter.empty
295
+ default = None if required else param.default
296
+
297
+ parameters.append(ToolParameter(
298
+ name=param_name,
299
+ param_type=param_type,
300
+ description=f"参数 {param_name}",
301
+ required=required,
302
+ default=default
303
+ ))
304
+
305
+ tool_def = ToolDefinition(
306
+ name=tool_name,
307
+ description=tool_desc,
308
+ parameters=parameters,
309
+ handler=func
310
+ )
311
+
312
+ func._tool_definition = tool_def # type: ignore[attr-defined]
313
+
314
+ return func
315
+
316
+ return decorator
317
+
318
+
319
+ def call_with_tools(
320
+ prompt: str,
321
+ tools: List[Union[ToolDefinition, Callable]],
322
+ context: Optional[CognitiveContext] = None,
323
+ *,
324
+ max_iterations: int = 5,
325
+ auto_execute: bool = True,
326
+ **kwargs
327
+ ) -> str:
328
+ """
329
+ 执行带工具调用的认知请求
330
+
331
+ 参数:
332
+ prompt: 用户提示
333
+ tools: 工具列表(ToolDefinition 或带 @tool 装饰的函数)
334
+ context: 认知上下文
335
+ max_iterations: 最大工具调用迭代次数
336
+ auto_execute: 是否自动执行工具调用
337
+ **kwargs: 其他参数传递给 cognitive_call
338
+
339
+ 返回:
340
+ 最终响应文本
341
+
342
+ 示例:
343
+ @tool
344
+ def search(query: str) -> str:
345
+ return f"搜索结果: {query}"
346
+
347
+ result = call_with_tools(
348
+ "搜索 Python 教程",
349
+ tools=[search]
350
+ )
351
+ """
352
+ try:
353
+ import openai
354
+ except ImportError:
355
+ raise ImportError("需要安装 openai 包。运行: pip install openai")
356
+
357
+ ctx = context or CognitiveContext.get_current()
358
+
359
+ if ctx is None:
360
+ config = LLMConfig()
361
+ else:
362
+ config = ctx.get_config()
363
+
364
+ if not config.api_key:
365
+ raise ValueError("未配置 API 密钥。")
366
+
367
+ # 构建工具列表
368
+ tool_defs = []
369
+ tool_handlers = {}
370
+
371
+ for t in tools:
372
+ if isinstance(t, ToolDefinition):
373
+ tool_defs.append(t)
374
+ if t.handler:
375
+ tool_handlers[t.name] = t.handler
376
+ elif hasattr(t, '_tool_definition'):
377
+ td = t._tool_definition # type: ignore[attr-defined]
378
+ tool_defs.append(td)
379
+ tool_handlers[td.name] = t
380
+ elif callable(t):
381
+ # 从函数创建工具定义
382
+ td = ToolDefinition(
383
+ name=t.__name__,
384
+ description=t.__doc__ or f"执行 {t.__name__}",
385
+ parameters=[],
386
+ handler=t
387
+ )
388
+ tool_defs.append(td)
389
+ tool_handlers[td.name] = t
390
+
391
+ client = openai.OpenAI(
392
+ api_key=config.api_key,
393
+ base_url=config.base_url
394
+ )
395
+
396
+ messages: List[Dict[str, Any]] = []
397
+ if ctx:
398
+ messages.extend(ctx.get_memory())
399
+ messages.append({"role": "user", "content": prompt})
400
+
401
+ openai_tools = [td.to_openai_tool() for td in tool_defs]
402
+
403
+ iteration = 0
404
+ while iteration < max_iterations:
405
+ response = client.chat.completions.create( # type: ignore[call-overload]
406
+ model=config.model,
407
+ messages=messages,
408
+ tools=openai_tools,
409
+ tool_choice="auto",
410
+ temperature=config.temperature,
411
+ max_tokens=config.max_tokens
412
+ )
413
+
414
+ message = response.choices[0].message
415
+
416
+ # 没有工具调用,返回结果
417
+ if not message.tool_calls:
418
+ result = message.content or ""
419
+ if ctx:
420
+ ctx.add_to_memory("user", prompt)
421
+ ctx.add_to_memory("assistant", result)
422
+ return result
423
+
424
+ # 有工具调用
425
+ messages.append(message)
426
+
427
+ for tool_call in message.tool_calls:
428
+ tc = ToolCall(
429
+ id=tool_call.id,
430
+ name=tool_call.function.name,
431
+ arguments=json.loads(tool_call.function.arguments)
432
+ )
433
+
434
+ if auto_execute and tc.name in tool_handlers:
435
+ try:
436
+ result = tool_handlers[tc.name](**tc.arguments)
437
+ tool_result = ToolResult(
438
+ tool_call_id=tc.id,
439
+ name=tc.name,
440
+ arguments=tc.arguments,
441
+ result=result
442
+ )
443
+ except Exception as e:
444
+ tool_result = ToolResult(
445
+ tool_call_id=tc.id,
446
+ name=tc.name,
447
+ arguments=tc.arguments,
448
+ result=None,
449
+ error=str(e)
450
+ )
451
+
452
+ messages.append(tool_result.to_openai_format())
453
+
454
+ iteration += 1
455
+
456
+ # 达到最大迭代次数,返回最后响应
457
+ final_response = client.chat.completions.create(
458
+ model=config.model,
459
+ messages=messages, # type: ignore[arg-type]
460
+ temperature=config.temperature,
461
+ max_tokens=config.max_tokens
462
+ )
463
+
464
+ return final_response.choices[0].message.content or ""
465
+
466
+
467
+ # 全局工具注册表
468
+ _global_registry = ToolRegistry()
469
+
470
+
471
+ def register_tool(
472
+ name: Optional[str] = None,
473
+ description: str = ""
474
+ ) -> Callable:
475
+ """注册工具到全局注册表"""
476
+ return _global_registry.register(name=name, description=description)
477
+
478
+
479
+ def get_global_registry() -> ToolRegistry:
480
+ """获取全局工具注册表"""
481
+ return _global_registry
@@ -0,0 +1,155 @@
1
+ """
2
+ AST 转换器模块
3
+
4
+ 将 `~"prompt"` 语法转换为 `cognitive_call("prompt")` 调用。
5
+ """
6
+
7
+ import ast
8
+ import types
9
+ from typing import Optional
10
+
11
+
12
+ class CognitiveTransformer(ast.NodeTransformer):
13
+ """
14
+ AST 转换器:将认知操作符转换为运行时调用。
15
+
16
+ 转换规则:
17
+ ~"prompt" → cognitive_call("prompt")
18
+ ~variable → cognitive_call(variable)
19
+ ~(expr) → cognitive_call(expr)
20
+ """
21
+
22
+ def __init__(self, context_name: str = "__cognitive_context__"):
23
+ super().__init__()
24
+ self.context_name = context_name
25
+
26
+ def _transform_invert(self, node: ast.UnaryOp) -> ast.Call:
27
+ """
28
+ 处理 `~` 操作符(一元取反操作符被借用为认知操作符)
29
+
30
+ 将 ~expr 转换为 cognitive_call(expr)
31
+ """
32
+ # 递归处理嵌套的节点
33
+ operand = self.visit(node.operand)
34
+
35
+ # 构建 cognitive_call(...) 调用
36
+ call = ast.Call(
37
+ func=ast.Attribute(
38
+ value=ast.Name(id="codegnipy", ctx=ast.Load()),
39
+ attr="cognitive_call",
40
+ ctx=ast.Load()
41
+ ),
42
+ args=[operand],
43
+ keywords=[
44
+ ast.keyword(
45
+ arg="context",
46
+ value=ast.Name(id=self.context_name, ctx=ast.Load())
47
+ )
48
+ ]
49
+ )
50
+
51
+ # 设置行号信息用于调试
52
+ ast.copy_location(call, node)
53
+ ast.fix_missing_locations(call)
54
+
55
+ return call
56
+
57
+ def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST:
58
+ """
59
+ 处理一元操作
60
+
61
+ 只有 `~` 操作符需要特殊处理,其他保持原样。
62
+ """
63
+ if isinstance(node.op, ast.Invert):
64
+ return self._transform_invert(node)
65
+
66
+ # 其他一元操作符保持原样
67
+ return self.generic_visit(node)
68
+
69
+
70
+ def transform_code(source: str, filename: str = "<codegnipy>") -> ast.Module:
71
+ """
72
+ 转换 Codegnipy 源代码,返回转换后的 AST。
73
+
74
+ 参数:
75
+ source: Python 源代码字符串
76
+ filename: 文件名(用于错误信息)
77
+
78
+ 返回:
79
+ 转换后的 AST 模块
80
+
81
+ 示例:
82
+ source = 'result = ~"你好"'
83
+ tree = transform_code(source)
84
+ # tree 现在包含: result = codegnipy.cognitive_call("你好", context=__cognitive_context__)
85
+ """
86
+ # 解析源代码
87
+ tree = ast.parse(source, filename=filename)
88
+
89
+ # 应用转换
90
+ transformer = CognitiveTransformer()
91
+ new_tree = transformer.visit(tree)
92
+
93
+ # 修复位置信息
94
+ ast.fix_missing_locations(new_tree)
95
+
96
+ return new_tree
97
+
98
+
99
+ def compile_codegnipy(
100
+ source: str,
101
+ filename: str = "<codegnipy>",
102
+ mode: str = "exec"
103
+ ) -> types.CodeType:
104
+ """
105
+ 编译 Codegnipy 源代码,返回代码对象。
106
+
107
+ 参数:
108
+ source: Python 源代码字符串
109
+ filename: 文件名
110
+ mode: 编译模式 ('exec', 'eval', 'single')
111
+
112
+ 返回:
113
+ 编译后的代码对象
114
+ """
115
+ tree = transform_code(source, filename)
116
+ return compile(tree, filename, mode)
117
+
118
+
119
+ def exec_codegnipy(
120
+ source: str,
121
+ globals_: Optional[dict] = None,
122
+ locals_: Optional[dict] = None,
123
+ filename: str = "<codegnipy>"
124
+ ) -> dict:
125
+ """
126
+ 执行 Codegnipy 源代码。
127
+
128
+ 参数:
129
+ source: Python 源代码字符串
130
+ globals_: 全局命名空间
131
+ locals_: 局部命名空间
132
+ filename: 文件名
133
+
134
+ 返回:
135
+ 执行后的局部命名空间
136
+ """
137
+ from .runtime import CognitiveContext
138
+
139
+ if globals_ is None:
140
+ globals_ = {}
141
+ if locals_ is None:
142
+ locals_ = {}
143
+
144
+ # 确保 codegnipy 模块可用
145
+ import codegnipy
146
+ globals_['codegnipy'] = codegnipy
147
+
148
+ # 创建上下文变量
149
+ globals_['__cognitive_context__'] = CognitiveContext.get_current()
150
+
151
+ # 编译并执行
152
+ code = compile_codegnipy(source, filename)
153
+ exec(code, globals_, locals_)
154
+
155
+ return locals_