AIPodCli 0.1.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.
ai_pod_cli/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """AIPodCli - AI-native IoC container low-code engine CLI."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from ai_pod_cli.context import PipelineContext
6
+
7
+ __all__ = ["PipelineContext"]
ai_pod_cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m ai_pod_cli`."""
2
+
3
+ from ai_pod_cli.cli import main
4
+
5
+ main()
ai_pod_cli/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ """CLI entry point — argparse setup and command dispatch."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from ai_pod_cli.config import init_config_if_not_exists
7
+ from ai_pod_cli.commands.add import handle_add
8
+ from ai_pod_cli.commands.build import handle_build
9
+ from ai_pod_cli.commands.create import handle_create
10
+ from ai_pod_cli.commands.init import handle_init
11
+
12
+
13
+ def main():
14
+ """Main CLI entry point for the AI Pod engine."""
15
+ # 从 .env 文件加载环境变量(优先于系统环境变量)
16
+ from dotenv import load_dotenv
17
+ load_dotenv()
18
+
19
+ # Windows 终端 GBK 兼容:强制 UTF-8 输出
20
+ if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
21
+ sys.stdout.reconfigure(encoding="utf-8")
22
+ if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
23
+ sys.stderr.reconfigure(encoding="utf-8")
24
+
25
+ # init 命令自行管理初始化,其余命令走自动初始化
26
+ if len(sys.argv) > 1 and sys.argv[1] != "init":
27
+ init_config_if_not_exists()
28
+
29
+ parser = argparse.ArgumentParser(description="AI 原生 IoC 容器低代码引擎 CLI")
30
+ subparsers = parser.add_subparsers(dest="command", required=True)
31
+
32
+ # 1. init 命令定义
33
+ init_parser = subparsers.add_parser("init", help="初始化项目:创建目录、配置,可选 AI 生成入口文件")
34
+ init_parser.add_argument("desc", nargs="?", default="", help="项目描述(AI 据此判断技术栈并生成入口文件)")
35
+ init_parser.add_argument("--install-deps", action="store_true", help="自动安装 Python 依赖")
36
+
37
+ # 2. create 命令定义
38
+ create_parser = subparsers.add_parser("create", help="让 AI 创造一个组件并记录配置")
39
+ create_parser.add_argument("--category", choices=["entry", "entity"], required=True, help="组件分类")
40
+ create_parser.add_argument("--name", required=True, help="组件名称")
41
+ create_parser.add_argument("--desc", required=True, help="给AI传入的需求和描述")
42
+
43
+ # 3. add 命令定义
44
+ add_parser = subparsers.add_parser("add", help="注册一个人工编写的实体组件到配置中心")
45
+ add_parser.add_argument("--category", choices=["entry", "entity"], default="entity", help="组件分类(默认 entity)")
46
+ add_parser.add_argument("--name", required=True, help="组件名称(必须与类名一致)")
47
+ add_parser.add_argument("--class-path", required=True, help="完整的类路径,如 mypackage.mymodule.MyClass")
48
+ add_parser.add_argument("--desc", required=True, help="组件功能描述")
49
+
50
+ # 4. build 命令定义
51
+ build_parser = subparsers.add_parser("build", help="AI 生成 pipeline 文件并注册到 routes.toml(只生成不执行)")
52
+ build_parser.add_argument("cmd", nargs="?", default="", help="自然语言业务指令(AI 据此规划执行链)")
53
+ build_parser.add_argument("--name", default="", help="保存 pipeline 的文件名(默认从指令自动生成)")
54
+ build_parser.add_argument("--list", action="store_true", help="列出所有已保存的 pipeline")
55
+
56
+ args = parser.parse_args()
57
+
58
+ if args.command == "init":
59
+ handle_init(args)
60
+ elif args.command == "create":
61
+ handle_create(args)
62
+ elif args.command == "add":
63
+ handle_add(args)
64
+ elif args.command == "build":
65
+ handle_build(args)
ai_pod_cli/client.py ADDED
@@ -0,0 +1,124 @@
1
+ """OpenAI client initialization with lazy loading and retry logic."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+
7
+ from openai import OpenAI
8
+
9
+
10
+ _client: OpenAI | None = None
11
+ _model: str | None = None
12
+
13
+ # 默认重试配置
14
+ DEFAULT_MAX_RETRIES = 3
15
+ DEFAULT_RETRY_DELAY = 2 # 秒
16
+
17
+
18
+ def get_client() -> OpenAI:
19
+ """Get or create the OpenAI client instance (lazy singleton).
20
+
21
+ Reads configuration from environment variables:
22
+ - OPENAI_API_KEY: API key (required)
23
+ - OPENAI_BASE_URL: API base URL (defaults to https://api.openai.com/v1)
24
+ """
25
+ global _client
26
+ if _client is None:
27
+ _client = OpenAI(
28
+ api_key=os.environ.get("OPENAI_API_KEY"),
29
+ base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
30
+ )
31
+ return _client
32
+
33
+
34
+ def get_model() -> str:
35
+ """Get the model name from environment (defaults to deepseek-chat)."""
36
+ global _model
37
+ if _model is None:
38
+ _model = os.environ.get("OPENAI_MODEL", "deepseek-chat")
39
+ return _model
40
+
41
+
42
+ def call_llm(
43
+ system_prompt: str,
44
+ user_content: str,
45
+ *,
46
+ json_mode: bool = False,
47
+ temperature: float = 0.1,
48
+ max_retries: int = DEFAULT_MAX_RETRIES,
49
+ retry_delay: float = DEFAULT_RETRY_DELAY,
50
+ ) -> dict | str:
51
+ """Call the LLM with retry on network errors or invalid JSON.
52
+
53
+ Args:
54
+ system_prompt: System prompt for the model.
55
+ user_content: User message content.
56
+ json_mode: If True, forces JSON output and parses the result into a dict.
57
+ temperature: Sampling temperature.
58
+ max_retries: Maximum number of retry attempts (default 3).
59
+ retry_delay: Base delay in seconds between retries (doubles each attempt).
60
+
61
+ Returns:
62
+ Parsed dict if json_mode=True, otherwise raw string content.
63
+
64
+ Raises:
65
+ RuntimeError: If all retries are exhausted.
66
+ """
67
+ client = get_client()
68
+ model = get_model()
69
+
70
+ kwargs = {
71
+ "model": model,
72
+ "messages": [
73
+ {"role": "system", "content": system_prompt},
74
+ {"role": "user", "content": user_content},
75
+ ],
76
+ "temperature": temperature,
77
+ }
78
+ if json_mode:
79
+ kwargs["response_format"] = {"type": "json_object"}
80
+
81
+ last_error: Exception | None = None
82
+ delay = retry_delay
83
+
84
+ for attempt in range(1, max_retries + 1):
85
+ try:
86
+ if attempt > 1:
87
+ print(f" [retry] 第 {attempt}/{max_retries} 次重试...")
88
+
89
+ response = client.chat.completions.create(**kwargs)
90
+ raw_content = response.choices[0].message.content
91
+
92
+ if json_mode:
93
+ # 尝试解析 JSON
94
+ try:
95
+ result = json.loads(raw_content)
96
+ except (json.JSONDecodeError, TypeError) as e:
97
+ last_error = ValueError(f"AI 返回的内容不是合法 JSON: {e}\n原始内容: {raw_content[:300]}")
98
+ print(f" [retry] 第 {attempt} 次: JSON 解析失败")
99
+ if attempt < max_retries:
100
+ time.sleep(delay)
101
+ delay *= 2
102
+ continue
103
+
104
+ # 检查 code 字段是否为空(仅 create/start 场景)
105
+ if "code" in result and not result.get("code", "").strip():
106
+ last_error = ValueError("AI 返回的 code 字段为空")
107
+ print(f" [retry] 第 {attempt} 次: code 字段为空")
108
+ if attempt < max_retries:
109
+ time.sleep(delay)
110
+ delay *= 2
111
+ continue
112
+
113
+ return result
114
+
115
+ return raw_content
116
+
117
+ except Exception as e:
118
+ last_error = e
119
+ print(f" [retry] 第 {attempt} 次: API 调用失败 ({type(e).__name__})")
120
+ if attempt < max_retries:
121
+ time.sleep(delay)
122
+ delay *= 2
123
+
124
+ raise RuntimeError(f"LLM 调用在 {max_retries} 次重试后仍然失败: {last_error}")
@@ -0,0 +1 @@
1
+ """CLI command implementations."""
@@ -0,0 +1,28 @@
1
+ """`add` command — register a human-written entity into the bean config."""
2
+
3
+ from ai_pod_cli.config import load_config, save_config
4
+
5
+
6
+ def handle_add(args):
7
+ """【add 命令】将人工编写的实体组件注册到配置中心"""
8
+ print(f"📦 [CLI] 正在注册人工实体: '{args.name}'...")
9
+
10
+ config = load_config()
11
+
12
+ # 检查是否已存在同名组件,存在则覆盖
13
+ config["beans"] = [b for b in config["beans"] if b["id"] != args.name]
14
+
15
+ new_bean = {
16
+ "id": args.name,
17
+ "category": args.category,
18
+ "type": "human_added",
19
+ "class_path": args.class_path,
20
+ "description": args.desc,
21
+ }
22
+
23
+ config["beans"].append(new_bean)
24
+ save_config(config)
25
+
26
+ print(f"💾 [注册成功] 实体 '{args.name}' 已写入配置中心 (class_path: {args.class_path})")
27
+ print(f" 分类: {args.category}")
28
+ print(f" 描述: {args.desc}")
@@ -0,0 +1,226 @@
1
+ """`build` command — AI generates pipeline file and registers it in routes.toml."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from datetime import datetime
7
+
8
+ from ai_pod_cli.client import call_llm
9
+ from ai_pod_cli.config import (
10
+ load_config, load_config_toml_keys, PIPELINES_DIR, register_route,
11
+ )
12
+ from ai_pod_cli.security import validate_code
13
+
14
+
15
+ def _slugify(text: str) -> str:
16
+ """将中文或任意文本转为安全的文件名。"""
17
+ safe = re.sub(r'[^\w一-鿿]+', '_', text.strip())
18
+ return safe[:60] or "pipeline"
19
+
20
+
21
+ def _save_pipeline(code: str, name: str, instruction: str) -> str:
22
+ """将 AI 生成的 pipeline Python 代码保存到 pipelines/ 目录。"""
23
+ os.makedirs(PIPELINES_DIR, exist_ok=True)
24
+ filename = f"{name}.py"
25
+ filepath = os.path.join(PIPELINES_DIR, filename)
26
+
27
+ header = (
28
+ f'"""Pipeline: {instruction}\n'
29
+ f'Generated: {datetime.now().isoformat()}\n'
30
+ f'"""'
31
+ )
32
+ full_code = header + "\n\n" + code
33
+
34
+ with open(filepath, "w", encoding="utf-8") as f:
35
+ f.write(full_code)
36
+
37
+ return filepath
38
+
39
+
40
+ def _list_pipelines() -> list[dict]:
41
+ """列出所有已保存的 pipeline (.py 文件)。"""
42
+ if not os.path.exists(PIPELINES_DIR):
43
+ return []
44
+
45
+ pipelines = []
46
+ for f in sorted(os.listdir(PIPELINES_DIR)):
47
+ if f == "__init__.py":
48
+ continue
49
+
50
+ filepath = os.path.join(PIPELINES_DIR, f)
51
+
52
+ if f.endswith(".py"):
53
+ instruction = ""
54
+ try:
55
+ with open(filepath, "r", encoding="utf-8") as fh:
56
+ first_lines = "".join(fh.readline() for _ in range(3))
57
+ match = re.search(r'Pipeline:\s*(.+)', first_lines)
58
+ if match:
59
+ instruction = match.group(1).strip()
60
+ except Exception:
61
+ pass
62
+
63
+ pipelines.append({
64
+ "file": f,
65
+ "name": f.replace(".py", ""),
66
+ "instruction": instruction,
67
+ })
68
+
69
+ return pipelines
70
+
71
+
72
+ def handle_build(args):
73
+ """【build 命令】AI 编排器:生成 pipeline 文件 → 注册到 routes.toml"""
74
+
75
+ # --- --list: 列出所有已保存的 pipeline ---
76
+ if args.list:
77
+ pipelines = _list_pipelines()
78
+ if not pipelines:
79
+ print(f"📂 {PIPELINES_DIR}/ 目录为空,还没有保存任何 pipeline。")
80
+ return
81
+
82
+ print(f"📂 已保存的 Pipeline ({len(pipelines)} 条):\n")
83
+ for p in pipelines:
84
+ print(f" 🐍 {p['file']}")
85
+ print(f" 指令: {p['instruction']}")
86
+ print()
87
+ return
88
+
89
+ # --- 默认: AI 生成新的 Python pipeline ---
90
+ if not args.cmd:
91
+ print("❌ 请提供指令描述。")
92
+ return
93
+
94
+ print(f"🎬 [build] 人类宏观指令: '{args.cmd}'")
95
+
96
+ config = load_config()
97
+ existing_beans_context = json.dumps(config["beans"], indent=2, ensure_ascii=False)
98
+ toml_keys = load_config_toml_keys()
99
+
100
+ # 收集所有组件的 class_path 用于生成 import
101
+ component_imports = {}
102
+ for bean in config["beans"]:
103
+ cid = bean["id"]
104
+ cpath = bean["class_path"]
105
+ module_path, class_name = cpath.rsplit(".", 1)
106
+ component_imports[cid] = {"module": module_path, "class": class_name}
107
+
108
+ imports_hint = "\n".join(
109
+ f" - {cid}: from {info['module']} import {info['class']}"
110
+ for cid, info in component_imports.items()
111
+ )
112
+
113
+ system_prompt = f"""
114
+ 你是一个智能编排引擎。当前系统中注册了以下组件账本:
115
+ {existing_beans_context}
116
+
117
+ 各组件的 import 路径:
118
+ {imports_hint}
119
+
120
+ 当前 config.toml 中可用的配置段和键名:
121
+ {toml_keys}
122
+ 组件通过注入 ConfigStore(from ai_pod_cli.config_store import ConfigStore)并用 config_store.get("section.key", default) 读取配置。
123
+
124
+ 你的任务是:根据人类的自然语言指令,生成一个完整的 Python pipeline 脚本。
125
+
126
+ 【生成的代码规范】:
127
+ 1. 必须定义一个 `run(ctx)` 函数作为入口,ctx 是 PipelineContext 类型。
128
+ 2. 在 run 函数内部:
129
+ - 从 ai_pod_cli.config import load_config
130
+ - 从 ai_pod_cli.container import build_container, Pod
131
+ - 用 build_container(config) 构建 DI 容器
132
+ - 用 S = Pod(container) 创建管道包装器
133
+ - 用 S(ComponentClass) 获取可管道串联的组件引用
134
+ 3. 使用管道符 | 串联组件:
135
+ (S(组件A) | S(组件B)).execute_all(ctx)
136
+ 这会自动依次执行各组件并记录轨迹。
137
+ 4. 从 ctx.params 读取入口参数,通过 ctx.set() 传递数据。
138
+ 5. 需要条件分支时,用 if/else 分别串联不同的管道。
139
+ 6. 最后 return ctx.summary()。
140
+ 7. 加上清晰的中文注释。
141
+
142
+ 【PipelineContext 的 API】:
143
+ - ctx.params: dict — 入口参数
144
+ - ctx.set(key, value): 写入数据池
145
+ - ctx.get(key, default=None): 读取数据池
146
+ - ctx.record_step(component_id, result): 记录执行步骤
147
+ - ctx.summary(): 返回执行摘要 dict
148
+
149
+ 【代码模板示例】:
150
+ ```python
151
+ from ai_pod_cli.context import PipelineContext
152
+ from ai_pod_cli.config import load_config
153
+ from ai_pod_cli.container import build_container, Pod
154
+ from modules.stockchecker import StockChecker
155
+ from modules.stocknotifier import StockNotifier
156
+
157
+
158
+ def run(ctx: PipelineContext):
159
+ config = load_config()
160
+ container = build_container(config)
161
+ S = Pod(container)
162
+
163
+ # 步骤 1: 检查库存
164
+ (S(StockChecker)).execute_all(ctx)
165
+
166
+ # 条件分支:库存不足时通知管理员
167
+ if ctx.get("stock", 0) <= 0:
168
+ (S(StockNotifier)).execute_all(ctx)
169
+
170
+ return ctx.summary()
171
+ ```
172
+
173
+ 【多组件串联示例】:
174
+ ```python
175
+ # 依次执行 A → B → C(自动记录每步轨迹)
176
+ (S(ComponentA) | S(ComponentB) | S(ComponentC)).execute_all(ctx)
177
+ ```
178
+
179
+ 请严格以标准 JSON 格式返回(不要包含 Markdown 块标记):
180
+ {{
181
+ "pipeline_ids": ["组件ID_1", "组件ID_2"],
182
+ "code": "完整的 Python pipeline 脚本代码(只包含代码,不含 ```python 标记)"
183
+ }}
184
+ """
185
+
186
+ try:
187
+ result = call_llm(system_prompt, f"指令: {args.cmd}", json_mode=True, temperature=0.1)
188
+
189
+ pipeline_ids = result.get("pipeline_ids", [])
190
+ generated_code = result.get("code", "")
191
+
192
+ print(f"🔗 [AI 编排] 执行链: {' → '.join(pipeline_ids) if pipeline_ids else '(空)'}")
193
+
194
+ if not generated_code:
195
+ print("❌ AI 未返回有效代码。")
196
+ return
197
+
198
+ except Exception as e:
199
+ print(f"❌ AI 编排失败: {e}")
200
+ return
201
+
202
+ # 安全检查
203
+ violations = validate_code(generated_code, for_pipeline=True)
204
+ if violations:
205
+ print(f"🛡️ [安全检查失败] Pipeline 代码包含 {len(violations)} 处违规:")
206
+ for v in violations:
207
+ print(f" ❌ {v}")
208
+ print(f"\n Pipeline 未保存。请调整描述重试。")
209
+ return
210
+ print(f"🛡️ [安全检查通过]")
211
+
212
+ # 保存 pipeline 文件
213
+ name = args.name or _slugify(args.cmd)
214
+ filepath = _save_pipeline(generated_code, name, args.cmd)
215
+ print(f"💾 [Pipeline 已保存] {filepath}")
216
+
217
+ # 注册到 routes.toml
218
+ register_route(
219
+ name=name,
220
+ pipeline_path=filepath,
221
+ description=args.cmd,
222
+ )
223
+ print(f"📋 [路由已注册] {name} → {filepath}")
224
+
225
+ print(f"\n🎉 [build 完成] Pipeline 已生成并注册。")
226
+ print(f" 运行方式: 通过你的入口文件调用 PipelineRunner().run(\"{name}\", params)")