aacf 0.8.0__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.
- aacf/__init__.py +42 -0
- aacf/_messages.py +155 -0
- aacf/cli.py +408 -0
- aacf/compiler.py +1243 -0
- aacf/core.py +1177 -0
- aacf/py.typed +1 -0
- aacf/visualize.py +415 -0
- aacf-0.8.0.dist-info/METADATA +285 -0
- aacf-0.8.0.dist-info/RECORD +13 -0
- aacf-0.8.0.dist-info/WHEEL +5 -0
- aacf-0.8.0.dist-info/entry_points.txt +2 -0
- aacf-0.8.0.dist-info/licenses/LICENSE +673 -0
- aacf-0.8.0.dist-info/top_level.txt +1 -0
aacf/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
# Copyright (C) 2026 AACF Contributors
|
|
3
|
+
#
|
|
4
|
+
# AACF — Agentic AI Compiler Framework / 智能体编译器框架
|
|
5
|
+
#
|
|
6
|
+
# AI workflow pre-compilation and highly controllable scheduling execution engine.
|
|
7
|
+
# AI 工作流预编译与高度可控的调度执行引擎。
|
|
8
|
+
#
|
|
9
|
+
# Exports / 导出:
|
|
10
|
+
# AACF: Application registry with @app.node() decorator / 应用注册表与节点装饰器
|
|
11
|
+
# LLMConfig: LLM call configuration / 大模型调用配置
|
|
12
|
+
# NodeStatus: Node execution status enum / 节点执行状态枚举
|
|
13
|
+
# AtomicNodeConfig: Atomic node execution configuration / 原子节点执行配置
|
|
14
|
+
# ExecutionResult: Rust-style execution result / Rust 风格的执行结果
|
|
15
|
+
# AACFError: Base error class / 基础错误类
|
|
16
|
+
# CircularDependencyError: Circular dependency error / 循环依赖错误
|
|
17
|
+
# NodeExecutionError: Node execution error / 节点执行错误
|
|
18
|
+
# PipelineError: Pipeline-level error / 管道级错误
|
|
19
|
+
# DAGCache: Incremental cache for DAG execution / DAG 执行增量缓存
|
|
20
|
+
# DAGVisualizer: Interactive HTML visualization / 交互式 HTML 可视化
|
|
21
|
+
|
|
22
|
+
from aacf.core import AACF, LLMConfig
|
|
23
|
+
from aacf.compiler import (
|
|
24
|
+
AtomicNodeConfig, NodeStatus,
|
|
25
|
+
ExecutionResult,
|
|
26
|
+
AACFError, CircularDependencyError, NodeExecutionError, PipelineError,
|
|
27
|
+
DAGCache,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from aacf.visualize import DAGVisualizer
|
|
32
|
+
except ImportError:
|
|
33
|
+
# pyvis not installed / pyvis 未安装
|
|
34
|
+
DAGVisualizer = None # type: ignore
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AACF", "LLMConfig",
|
|
38
|
+
"NodeStatus", "AtomicNodeConfig",
|
|
39
|
+
"ExecutionResult",
|
|
40
|
+
"AACFError", "CircularDependencyError", "NodeExecutionError", "PipelineError",
|
|
41
|
+
"DAGCache", "DAGVisualizer",
|
|
42
|
+
]
|
aacf/_messages.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
# Copyright (C) 2026 AACF Contributors
|
|
3
|
+
"""
|
|
4
|
+
AACF Messages — Bilingual prompt templates and docstring formats / 双语提示词模板与文档格式
|
|
5
|
+
|
|
6
|
+
This module centralizes all user-facing text templates for internationalization.
|
|
7
|
+
本模块集中管理所有面向用户的文本模板,用于国际化支持。
|
|
8
|
+
|
|
9
|
+
Currently supports: Chinese (zh) and English (en)
|
|
10
|
+
当前支持:中文 (zh) 和英文 (en)
|
|
11
|
+
|
|
12
|
+
Sections / 模块结构:
|
|
13
|
+
PROMPT_TEMPLATES: Templates for @app.node() prompt construction / 节点 prompt 构建模板
|
|
14
|
+
DOCSTRING_TEMPLATES: Templates for docstring injection / Docstring 注入模板
|
|
15
|
+
CLI_MESSAGES: Templates for CLI output messages / CLI 输出消息模板
|
|
16
|
+
get_template(): Helper function to retrieve templates by lang/category/key / 模板获取辅助函数
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ────────────────────────────────────────────────────────────
|
|
20
|
+
# Prompt Templates for @app.node() / @app.node() 提示词模板
|
|
21
|
+
# ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
PROMPT_TEMPLATES = {
|
|
24
|
+
"zh": {
|
|
25
|
+
"role": "你是{who},你在{where},你是一个{what}。\n",
|
|
26
|
+
"intent": "操作的意图原因是:{why}。\n",
|
|
27
|
+
"how": "你有以下操作选项:{how}\n",
|
|
28
|
+
"routing_header": "\n【重要:智能路由分发模式】\n",
|
|
29
|
+
"routing_modules": "你有权调用的下级专业子模块/节点有:{modules}\n",
|
|
30
|
+
"routing_instruction": "请深入分析用户的意图,自主判断应该去调用上述哪个子节点来完成任务。\n",
|
|
31
|
+
"routing_format": '你只需要输出一个纯 JSON 对象,格式为:\n',
|
|
32
|
+
"routing_example": '{"target_node": "节点名称", "reason": "调用原因", "inputs": {"参数名": "参数值"}}\n',
|
|
33
|
+
"routing_fallback": "如果你认为不需要调用任何子模块,请直接完成用户的需求。\n\n",
|
|
34
|
+
"output_request": "请根据输入内容,按照输出要求生成内容。\n",
|
|
35
|
+
"output_requirement": "输出要求:{out}\n",
|
|
36
|
+
},
|
|
37
|
+
"en": {
|
|
38
|
+
"role": "You are {who}, working in {where}, and your role is {what}.\n",
|
|
39
|
+
"intent": "The intent behind this operation is: {why}.\n",
|
|
40
|
+
"how": "You have the following operational options: {how}\n",
|
|
41
|
+
"routing_header": "\n[IMPORTANT: Intelligent Routing Mode]\n",
|
|
42
|
+
"routing_modules": "You have access to the following specialized sub-modules/nodes: {modules}\n",
|
|
43
|
+
"routing_instruction": "Please analyze the user's intent deeply and autonomously determine which sub-node to invoke.\n",
|
|
44
|
+
"routing_format": 'Output only a pure JSON object in this format:\n',
|
|
45
|
+
"routing_example": '{"target_node": "node_name", "reason": "reason_for_call", "inputs": {"param_name": "param_value"}}\n',
|
|
46
|
+
"routing_fallback": "If you determine no sub-module is needed, please fulfill the user's request directly.\n\n",
|
|
47
|
+
"output_request": "Please generate content based on the input according to the output requirements.\n",
|
|
48
|
+
"output_requirement": "Output requirements: {out}\n",
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ────────────────────────────────────────────────────────────
|
|
54
|
+
# Docstring Injection Templates / Docstring 注入模板
|
|
55
|
+
# ────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
DOCSTRING_TEMPLATES = {
|
|
58
|
+
"zh": {
|
|
59
|
+
"header": '🤖 【AACF 智能节点】: {who}\n',
|
|
60
|
+
"task": ' 核心任务: {what}\n',
|
|
61
|
+
"env": '📍 执行环境: {where}\n',
|
|
62
|
+
},
|
|
63
|
+
"en": {
|
|
64
|
+
"header": '🤖 [AACF Smart Node]: {who}\n',
|
|
65
|
+
"task": '🎯 Core Task: {what}\n',
|
|
66
|
+
"env": '📍 Environment: {where}\n',
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ────────────────────────────────────────────────────────────
|
|
72
|
+
# CLI Messages / CLI 输出消息
|
|
73
|
+
# ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
CLI_MESSAGES = {
|
|
76
|
+
"zh": {
|
|
77
|
+
"init_start": "正在初始化 AACF 项目:",
|
|
78
|
+
"init_creating": "正在创建项目结构...",
|
|
79
|
+
"init_done": "✔ 项目结构已创建。",
|
|
80
|
+
"init_venv": "✔ 虚拟环境已初始化。",
|
|
81
|
+
"init_next": "下一步:",
|
|
82
|
+
"run_start": "▶ 正在运行 AACF 脚本:",
|
|
83
|
+
"run_error": "✖ 脚本退出,错误码:",
|
|
84
|
+
"sync_scanning": "▶ 正在扫描 @app.node:",
|
|
85
|
+
"sync_injecting": "正在注入 docstring...",
|
|
86
|
+
"sync_success": "✔ 成功为 {count} 个文件注入 docstring。",
|
|
87
|
+
"sync_hint": "你的 IDE(如 Pylance)现在将显示完整的 docstring 和类型提示。",
|
|
88
|
+
"sync_none": "ℹ 未找到需要同步的 @app.node 函数。",
|
|
89
|
+
"doc_start": "▶ 正在启动 AACF 文档服务器:",
|
|
90
|
+
"doc_serving": "服务地址:http://localhost:{port}",
|
|
91
|
+
"doc_stop": "按 Ctrl+C 停止。",
|
|
92
|
+
"doc_missing": "✖ pdoc 未安装。请运行 `pip install pdoc`。",
|
|
93
|
+
"doc_stopped": "✔ 文档服务器已停止。",
|
|
94
|
+
"doc_error": "✖ 启动文档服务器时出错:",
|
|
95
|
+
"watch_start": "▶ 正在监听文件变化:",
|
|
96
|
+
"watch_stop": "按 Ctrl+C 停止。",
|
|
97
|
+
"watch_changes": "检测到 {count} 个文件变化,正在同步...",
|
|
98
|
+
"watch_injected": "✔ 已为 {count} 个文件注入 docstring。",
|
|
99
|
+
"watch_noop": "无需更新 @app.node。",
|
|
100
|
+
"watch_stopped": "✔ 监听器已停止。",
|
|
101
|
+
},
|
|
102
|
+
"en": {
|
|
103
|
+
"init_start": "Initializing AACF Project: ",
|
|
104
|
+
"init_creating": "Creating project structure...",
|
|
105
|
+
"init_done": "✔ Project structure created.",
|
|
106
|
+
"init_venv": "✔ Virtual environment initialized.",
|
|
107
|
+
"init_next": "Next steps:",
|
|
108
|
+
"run_start": "▶ Running AACF Script: ",
|
|
109
|
+
"run_error": "✖ Script exited with error code: ",
|
|
110
|
+
"sync_scanning": "▶ Scanning for @app.node in: ",
|
|
111
|
+
"sync_injecting": "Injecting docstrings...",
|
|
112
|
+
"sync_success": "✔ Successfully injected docstrings for {count} files.",
|
|
113
|
+
"sync_hint": "Your IDE (e.g., Pylance) will now show perfect docstrings and type hints.",
|
|
114
|
+
"sync_none": " No @app.node functions found to sync.",
|
|
115
|
+
"doc_start": "▶ Starting AACF Doc Server for: ",
|
|
116
|
+
"doc_serving": "Serving on http://localhost:{port}",
|
|
117
|
+
"doc_stop": "Press Ctrl+C to stop.",
|
|
118
|
+
"doc_missing": "✖ pdoc is not installed. Please run `pip install pdoc`.",
|
|
119
|
+
"doc_stopped": "✔ Doc server stopped.",
|
|
120
|
+
"doc_error": "✖ Error starting doc server: ",
|
|
121
|
+
"watch_start": "▶ Watching for changes in: ",
|
|
122
|
+
"watch_stop": "Press Ctrl+C to stop.",
|
|
123
|
+
"watch_changes": "Changes detected in {count} file(s). Syncing...",
|
|
124
|
+
"watch_injected": "✔ Injected docstrings for {count} file(s).",
|
|
125
|
+
"watch_noop": "No @app.node updates required.",
|
|
126
|
+
"watch_stopped": "✔ Watcher stopped.",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ────────────────────────────────────────────────────────────
|
|
132
|
+
# Helper Functions / 辅助函数
|
|
133
|
+
# ────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
def get_template(lang: str, category: str, key: str) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Get a template string by language, category, and key.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
lang: Language code ("zh" or "en")
|
|
141
|
+
category: Template category ("prompt", "docstring", "cli")
|
|
142
|
+
key: Template key within the category
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Template string, falling back to Chinese if language not found
|
|
146
|
+
"""
|
|
147
|
+
templates = {
|
|
148
|
+
"prompt": PROMPT_TEMPLATES,
|
|
149
|
+
"docstring": DOCSTRING_TEMPLATES,
|
|
150
|
+
"cli": CLI_MESSAGES,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
category_dict = templates.get(category, {})
|
|
154
|
+
lang_dict = category_dict.get(lang, category_dict.get("zh", {}))
|
|
155
|
+
return lang_dict.get(key, "")
|
aacf/cli.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
# Copyright (C) 2026 AACF Contributors
|
|
3
|
+
"""
|
|
4
|
+
AACF CLI — 命令行工具 / Command Line Interface
|
|
5
|
+
|
|
6
|
+
提供 aacf init / run / sync / watch / doc 等 CLI 子命令。
|
|
7
|
+
Provides CLI subcommands: init / run / sync / watch / doc.
|
|
8
|
+
"""
|
|
9
|
+
import ast
|
|
10
|
+
import importlib.util
|
|
11
|
+
import inspect
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
|
|
24
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
|
25
|
+
try:
|
|
26
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
27
|
+
except AttributeError:
|
|
28
|
+
pass
|
|
29
|
+
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
|
|
30
|
+
try:
|
|
31
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
32
|
+
except AttributeError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
name="aacf",
|
|
37
|
+
help="AACF: Agentic AI Compiler Framework CLI / 智能体编译器框架命令行工具",
|
|
38
|
+
add_completion=False,
|
|
39
|
+
)
|
|
40
|
+
console = Console()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ────────────────────────────────────────────
|
|
44
|
+
# Docstring 注入工具(供 sync / watch 使用)/ Docstring injection tool (for sync/watch)
|
|
45
|
+
# ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
def inject_docstrings_for_file(filepath: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
通过反射读取 Python 文件中的 @app.node 节点,并将 Docstring 注入到源码中。
|
|
50
|
+
Read Python file via reflection to find @app.node nodes and inject docstrings into source.
|
|
51
|
+
|
|
52
|
+
供 ``aacf sync`` 和 ``aacf watch`` 命令调用。
|
|
53
|
+
Called by ``aacf sync`` and ``aacf watch`` commands.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
filepath: 目标 Python 文件路径 / Target Python file path
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True 如果成功注入 / True if injection succeeded, False otherwise
|
|
60
|
+
"""
|
|
61
|
+
def _inject_docstrings_to_py(filepath, nodes):
|
|
62
|
+
"""
|
|
63
|
+
将 docstring 注入到单个 Python 文件中 / Inject docstrings into a single Python file.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
filepath: 目标文件路径 / Target file path
|
|
67
|
+
nodes: (函数对象, 元数据字典, 签名) 的列表 / List of (func, meta_dict, signature) tuples
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True 如果实际修改了文件 / True if the file was actually modified
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
74
|
+
source_lines = f.readlines()
|
|
75
|
+
tree = ast.parse(''.join(source_lines))
|
|
76
|
+
docstring_ranges = {}
|
|
77
|
+
for node in ast.walk(tree):
|
|
78
|
+
if isinstance(node, ast.FunctionDef):
|
|
79
|
+
doc = ast.get_docstring(node)
|
|
80
|
+
if doc and 'AACF' in doc:
|
|
81
|
+
expr_node = node.body[0]
|
|
82
|
+
docstring_ranges[node.name] = (expr_node.lineno, expr_node.end_lineno)
|
|
83
|
+
except Exception:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
funcs_to_inject = []
|
|
87
|
+
for func, meta, sig in nodes:
|
|
88
|
+
name = func.__name__
|
|
89
|
+
# 如果源码中已有 AACF docstring,跳过(幂等性)
|
|
90
|
+
# If source already has AACF docstring, skip (idempotent)
|
|
91
|
+
if name in docstring_ranges:
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
_, start_lineno = inspect.getsourcelines(func)
|
|
95
|
+
except Exception:
|
|
96
|
+
continue
|
|
97
|
+
funcs_to_inject.append((start_lineno, func, meta))
|
|
98
|
+
|
|
99
|
+
if not funcs_to_inject:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
funcs_to_inject.sort(key=lambda x: x[0], reverse=True)
|
|
103
|
+
|
|
104
|
+
for start_lineno, func, meta in funcs_to_inject:
|
|
105
|
+
name = func.__name__
|
|
106
|
+
if name in docstring_ranges:
|
|
107
|
+
start, end = docstring_ranges[name]
|
|
108
|
+
del source_lines[start - 1:end]
|
|
109
|
+
|
|
110
|
+
idx = start_lineno - 1
|
|
111
|
+
paren_count = 0
|
|
112
|
+
in_def = False
|
|
113
|
+
colon_idx = idx
|
|
114
|
+
def_indent = 0
|
|
115
|
+
for i in range(idx, len(source_lines)):
|
|
116
|
+
line = source_lines[i]
|
|
117
|
+
if not in_def and line.lstrip().startswith('def '):
|
|
118
|
+
in_def = True
|
|
119
|
+
def_indent = len(line) - len(line.lstrip())
|
|
120
|
+
paren_count += line.count('(') - line.count(')')
|
|
121
|
+
if in_def and paren_count == 0 and ':' in line:
|
|
122
|
+
colon_idx = i
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
indent_str = " " * (def_indent + 4)
|
|
126
|
+
who = meta.get("who", "未命名智能体 / AI Agent")
|
|
127
|
+
what = meta.get("what", "未定义任务 / Task undefined")
|
|
128
|
+
where = meta.get("where", "未知环境 / Environment unknown")
|
|
129
|
+
|
|
130
|
+
docstring_lines = [
|
|
131
|
+
f'{indent_str}"""\n',
|
|
132
|
+
f'{indent_str}🤖 【AACF 智能节点 / Smart Node】: {who}\n',
|
|
133
|
+
f'{indent_str}🎯 核心任务 / Core Task: {what}\n',
|
|
134
|
+
f'{indent_str}📍 执行环境 / Environment: {where}\n',
|
|
135
|
+
f'{indent_str}"""\n',
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
line = source_lines[colon_idx]
|
|
139
|
+
colon_pos = line.find(':', line.rfind(')'))
|
|
140
|
+
if colon_pos != -1:
|
|
141
|
+
before_colon = line[:colon_pos + 1]
|
|
142
|
+
after_colon = line[colon_pos + 1:].strip()
|
|
143
|
+
source_lines[colon_idx] = before_colon + '\n'
|
|
144
|
+
if after_colon:
|
|
145
|
+
docstring_lines.append(f'{indent_str}{after_colon}\n')
|
|
146
|
+
|
|
147
|
+
source_lines = source_lines[:colon_idx + 1] + docstring_lines + source_lines[colon_idx + 1:]
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
151
|
+
f.writelines(source_lines)
|
|
152
|
+
return True
|
|
153
|
+
except Exception:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
target_path = Path(filepath).resolve()
|
|
157
|
+
if not target_path.exists() or target_path.suffix != '.py':
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
module_name = target_path.stem
|
|
161
|
+
parent_dir = str(target_path.parent)
|
|
162
|
+
sys.path.insert(0, parent_dir)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
spec = importlib.util.spec_from_file_location(module_name, str(target_path))
|
|
166
|
+
if spec is None or spec.loader is None:
|
|
167
|
+
return False
|
|
168
|
+
module = importlib.util.module_from_spec(spec)
|
|
169
|
+
sys.modules[module_name] = module
|
|
170
|
+
spec.loader.exec_module(module)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"Failed to import {filepath}: {e}")
|
|
173
|
+
sys.path.pop(0)
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
sys.path.pop(0)
|
|
177
|
+
|
|
178
|
+
has_nodes = False
|
|
179
|
+
nodes = []
|
|
180
|
+
for name, obj in inspect.getmembers(module):
|
|
181
|
+
if inspect.isfunction(obj) and hasattr(obj, '__aacf_meta__'):
|
|
182
|
+
# 仅处理定义在当前文件中的函数,跳过从其他文件导入的函数
|
|
183
|
+
# Only process functions defined in the current file, skip imported ones
|
|
184
|
+
# 使用 inspect.unwrap 追溯 @wraps 包装链,获取原始函数的源文件
|
|
185
|
+
# Use inspect.unwrap to follow @wraps chain to get original function's source file
|
|
186
|
+
try:
|
|
187
|
+
original_func = inspect.unwrap(obj)
|
|
188
|
+
func_file = inspect.getsourcefile(original_func)
|
|
189
|
+
if func_file and Path(func_file).resolve() != target_path:
|
|
190
|
+
continue
|
|
191
|
+
except (OSError, TypeError):
|
|
192
|
+
continue
|
|
193
|
+
has_nodes = True
|
|
194
|
+
meta = obj.__aacf_meta__
|
|
195
|
+
sig = inspect.signature(obj)
|
|
196
|
+
nodes.append((obj, meta, sig))
|
|
197
|
+
|
|
198
|
+
if not has_nodes:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
return _inject_docstrings_to_py(filepath, nodes) or False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ────────────────────────────────────────────
|
|
205
|
+
# CLI Commands / 命令行命令
|
|
206
|
+
# ─────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def init(
|
|
210
|
+
project_name: str = typer.Argument(..., help="The name of your new AACF project / 新项目名称"),
|
|
211
|
+
):
|
|
212
|
+
"""Initialize a new AACF project with the recommended directory structure.
|
|
213
|
+
初始化一个新的 AACF 项目,包含推荐的目录结构。"""
|
|
214
|
+
console.print(Panel.fit(
|
|
215
|
+
f"[bold blue]Initializing AACF Project / 初始化项目:[/] [green]{project_name}[/]",
|
|
216
|
+
border_style="blue",
|
|
217
|
+
))
|
|
218
|
+
project_dir = Path.cwd() / project_name
|
|
219
|
+
if project_dir.exists():
|
|
220
|
+
console.print(f"[bold red]✖[/] Directory '{project_name}' already exists / 目录 '{project_name}' 已存在。")
|
|
221
|
+
raise typer.Exit(code=1)
|
|
222
|
+
|
|
223
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress:
|
|
224
|
+
progress.add_task(description="Creating project structure / 创建项目结构...", total=None)
|
|
225
|
+
# 创建项目目录 / Create project directory
|
|
226
|
+
project_dir.mkdir(parents=True)
|
|
227
|
+
(project_dir / "src").mkdir()
|
|
228
|
+
# 创建 main.py 模板 / Create main.py template
|
|
229
|
+
main_py = project_dir / "main.py"
|
|
230
|
+
main_py.write_text(
|
|
231
|
+
'from aacf import AACF, LLMConfig\n\n'
|
|
232
|
+
'app = AACF(__name__, config=LLMConfig(\n'
|
|
233
|
+
' model="qwen2.5-7b-instruct",\n'
|
|
234
|
+
' url="http://127.0.0.1:8080/v1/chat/completions",\n'
|
|
235
|
+
'))\n\n\n'
|
|
236
|
+
'@app.node("hello").who("助手").what("打招呼").build()\n'
|
|
237
|
+
'def hello(name: str):\n'
|
|
238
|
+
' pass\n\n\n'
|
|
239
|
+
'if __name__ == "__main__":\n'
|
|
240
|
+
' print(hello(name="World"))\n',
|
|
241
|
+
encoding="utf-8",
|
|
242
|
+
)
|
|
243
|
+
# 创建 __init__.py / Create __init__.py
|
|
244
|
+
(project_dir / "src" / "__init__.py").write_text("", encoding="utf-8")
|
|
245
|
+
# 初始化虚拟环境 / Initialize virtual environment
|
|
246
|
+
try:
|
|
247
|
+
subprocess.check_call(
|
|
248
|
+
[sys.executable, "-m", "venv", ".venv"],
|
|
249
|
+
cwd=str(project_dir),
|
|
250
|
+
stdout=subprocess.DEVNULL,
|
|
251
|
+
stderr=subprocess.DEVNULL,
|
|
252
|
+
)
|
|
253
|
+
except (subprocess.CalledProcessError, Exception):
|
|
254
|
+
pass # 虚拟环境创建失败不影响项目初始化 / venv failure is non-fatal
|
|
255
|
+
|
|
256
|
+
console.print("[bold green]✔[/] Project structure created / 项目结构已创建。")
|
|
257
|
+
console.print("[bold green]✔[/] Virtual environment initialized / 虚拟环境已初始化。")
|
|
258
|
+
console.print(f"\n[bold]Next steps / 下一步:[/]\n cd {project_name}\n aacf run main.py")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def run(
|
|
263
|
+
script: str = typer.Argument(..., help="The Python script to run (e.g., main.py) / 要运行的 Python 脚本"),
|
|
264
|
+
):
|
|
265
|
+
"""Execute an AACF python script with enhanced AI logging.
|
|
266
|
+
执行 AACF Python 脚本,增强 AI 日志输出。"""
|
|
267
|
+
console.print(f"[bold magenta]▶ Running AACF Script / 运行脚本:[/] {script}")
|
|
268
|
+
try:
|
|
269
|
+
subprocess.check_call([sys.executable, script])
|
|
270
|
+
except subprocess.CalledProcessError as e:
|
|
271
|
+
console.print(f"[bold red]✖ Script exited with error code / 脚本退出错误码 {e.returncode}[/]")
|
|
272
|
+
raise typer.Exit(code=e.returncode)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command()
|
|
276
|
+
def sync(
|
|
277
|
+
path: str = typer.Argument(".", help="Directory or file to scan for @app.node functions / 扫描目录或文件"),
|
|
278
|
+
):
|
|
279
|
+
"""Scan Python files and inject docstrings to source code for perfect IDE support.
|
|
280
|
+
扫描 Python 文件并向源码注入 docstring,提供完美的 IDE 支持。"""
|
|
281
|
+
console.print(f"[bold magenta]▶ Scanning for @app.node in / 扫描:[/] {path}")
|
|
282
|
+
target_path = Path(path).resolve()
|
|
283
|
+
|
|
284
|
+
files_to_scan = []
|
|
285
|
+
if target_path.is_file():
|
|
286
|
+
if target_path.suffix == '.py':
|
|
287
|
+
files_to_scan.append(target_path)
|
|
288
|
+
else:
|
|
289
|
+
files_to_scan = list(target_path.rglob("*.py"))
|
|
290
|
+
|
|
291
|
+
count = 0
|
|
292
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress:
|
|
293
|
+
task = progress.add_task(description="Injecting docstrings / 注入文档...", total=len(files_to_scan))
|
|
294
|
+
for py_file in files_to_scan:
|
|
295
|
+
if inject_docstrings_for_file(str(py_file)):
|
|
296
|
+
count += 1
|
|
297
|
+
progress.advance(task)
|
|
298
|
+
|
|
299
|
+
if count > 0:
|
|
300
|
+
console.print(f"[bold green]✔[/] Successfully injected docstrings for [bold]{count}[/] files / 成功注入 {count} 个文件。")
|
|
301
|
+
console.print("[dim]Your IDE (e.g., Pylance) will now show perfect docstrings and type hints. / IDE 现在将显示完整的文档和类型提示。[/]")
|
|
302
|
+
else:
|
|
303
|
+
console.print("[bold yellow]ℹ[/] No @app.node functions found to sync / 未找到需要同步的 @app.node 函数。")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@app.command()
|
|
307
|
+
def doc(
|
|
308
|
+
module: str = typer.Argument("aacf", help="The Python module or directory to document / 要生成文档的模块或目录"),
|
|
309
|
+
port: int = typer.Option(8080, "--port", "-p", help="Port to serve the documentation on / 文档服务端口"),
|
|
310
|
+
):
|
|
311
|
+
"""Zero-config API documentation server. (Inspired by Rust's cargo doc)
|
|
312
|
+
零配置 API 文档服务器。(灵感来自 Rust 的 cargo doc)"""
|
|
313
|
+
console.print(f"[bold magenta]▶ Starting AACF Doc Server for / 启动文档服务器:[/] {module}")
|
|
314
|
+
console.print(f"[dim]Serving on / 服务地址: http://127.0.0.1:{port}[/]")
|
|
315
|
+
console.print("[dim]Press Ctrl+C to stop / 按 Ctrl+C 停止。[/]")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
import pdoc # type: ignore
|
|
319
|
+
except ImportError:
|
|
320
|
+
console.print("[bold red]✖ pdoc is not installed. Please run `pip install pdoc`. / pdoc 未安装,请运行 `pip install pdoc`。[/]")
|
|
321
|
+
raise typer.Exit(code=1)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
import webbrowser
|
|
325
|
+
import threading
|
|
326
|
+
from pdoc.web import DocServer, open_browser
|
|
327
|
+
|
|
328
|
+
def launch():
|
|
329
|
+
time.sleep(1)
|
|
330
|
+
open_browser(f"http://127.0.0.1:{port}")
|
|
331
|
+
|
|
332
|
+
threading.Thread(target=launch, daemon=True).start()
|
|
333
|
+
with DocServer(("127.0.0.1", port), [module]) as httpd:
|
|
334
|
+
console.print(f"[bold green]✔[/] Doc server running / 文档服务器运行中: http://127.0.0.1:{port}")
|
|
335
|
+
httpd.serve_forever()
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
console.print("\n[bold green]✔[/] Doc server stopped / 文档服务器已停止。")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
console.print(f"[bold red]✖ Error starting doc server / 启动文档服务器出错:[/] {e}")
|
|
340
|
+
raise typer.Exit(code=1)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@app.command()
|
|
344
|
+
def watch(
|
|
345
|
+
path: str = typer.Argument(".", help="Directory to watch for changes / 监听变化的目录"),
|
|
346
|
+
interval: float = typer.Option(1.0, "--interval", "-i", help="Polling interval in seconds / 轮询间隔(秒)"),
|
|
347
|
+
):
|
|
348
|
+
"""Watch Python files for changes and automatically inject docstrings.
|
|
349
|
+
监听 Python 文件变化并自动注入 docstring。"""
|
|
350
|
+
console.print(f"[bold magenta]▶ Watching for changes in / 监听:[/] {path}")
|
|
351
|
+
console.print("[dim]Press Ctrl+C to stop / 按 Ctrl+C 停止。[/]")
|
|
352
|
+
|
|
353
|
+
target_path = Path(path).resolve()
|
|
354
|
+
last_mtimes = {}
|
|
355
|
+
|
|
356
|
+
def get_py_files():
|
|
357
|
+
if target_path.is_file():
|
|
358
|
+
return [target_path] if target_path.suffix == '.py' else []
|
|
359
|
+
return list(target_path.rglob("*.py"))
|
|
360
|
+
|
|
361
|
+
for py_file in get_py_files():
|
|
362
|
+
if "typings" in py_file.parts:
|
|
363
|
+
continue
|
|
364
|
+
try:
|
|
365
|
+
last_mtimes[py_file] = os.path.getmtime(py_file)
|
|
366
|
+
except OSError:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
while True:
|
|
371
|
+
time.sleep(interval)
|
|
372
|
+
changed_files = []
|
|
373
|
+
|
|
374
|
+
for py_file in get_py_files():
|
|
375
|
+
if "typings" in py_file.parts:
|
|
376
|
+
continue
|
|
377
|
+
try:
|
|
378
|
+
mtime = os.path.getmtime(py_file)
|
|
379
|
+
if py_file not in last_mtimes or mtime > last_mtimes[py_file]:
|
|
380
|
+
changed_files.append(py_file)
|
|
381
|
+
last_mtimes[py_file] = mtime
|
|
382
|
+
except OSError:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
if changed_files:
|
|
386
|
+
console.print(f"\n[bold cyan]Changes detected in {len(changed_files)} file(s). Syncing... / 检测到 {len(changed_files)} 个文件变化,正在同步...[/]")
|
|
387
|
+
count = 0
|
|
388
|
+
for py_file in changed_files:
|
|
389
|
+
if inject_docstrings_for_file(str(py_file)):
|
|
390
|
+
count += 1
|
|
391
|
+
if count > 0:
|
|
392
|
+
console.print(f"[bold green]✔[/] Injected docstrings for [bold]{count}[/] file(s) / 已注入 {count} 个文件。")
|
|
393
|
+
# 注入后更新 mtime,避免自身写入触发下一轮检测(防止死循环)
|
|
394
|
+
# Update mtime after injection to prevent self-write from triggering next detection
|
|
395
|
+
for py_file in changed_files:
|
|
396
|
+
try:
|
|
397
|
+
last_mtimes[py_file] = os.path.getmtime(py_file)
|
|
398
|
+
except OSError:
|
|
399
|
+
pass
|
|
400
|
+
else:
|
|
401
|
+
console.print("[dim]No @app.node updates required / 无需更新 @app.node。[/]")
|
|
402
|
+
|
|
403
|
+
except KeyboardInterrupt:
|
|
404
|
+
console.print("\n[bold green]✔[/] Watcher stopped / 监听器已停止。")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
app()
|