alayaflow 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.
- alayaflow/__init__.py +5 -0
- alayaflow/api/__init__.py +5 -0
- alayaflow/api/api_singleton.py +81 -0
- alayaflow/clients/alayamem/base_client.py +19 -0
- alayaflow/clients/alayamem/http_client.py +64 -0
- alayaflow/common/config.py +106 -0
- alayaflow/component/__init__.py +0 -0
- alayaflow/component/chat_model.py +20 -0
- alayaflow/component/intent_classifier.py +94 -0
- alayaflow/component/langflow/__init__.py +0 -0
- alayaflow/component/langflow/intent_classifier.py +83 -0
- alayaflow/component/llm_node.py +123 -0
- alayaflow/component/memory.py +50 -0
- alayaflow/component/retrieve_node.py +17 -0
- alayaflow/component/web_search.py +126 -0
- alayaflow/execution/__init__.py +6 -0
- alayaflow/execution/env_manager.py +424 -0
- alayaflow/execution/executor_manager.py +59 -0
- alayaflow/execution/executors/__init__.py +9 -0
- alayaflow/execution/executors/base_executor.py +9 -0
- alayaflow/execution/executors/naive_executor.py +121 -0
- alayaflow/execution/executors/uv_executor.py +125 -0
- alayaflow/execution/executors/worker_executor.py +12 -0
- alayaflow/execution/langfuse_tracing.py +104 -0
- alayaflow/execution/workflow_runner.py +98 -0
- alayaflow/utils/singleton.py +14 -0
- alayaflow/workflow/__init__.py +6 -0
- alayaflow/workflow/runnable/__init__.py +7 -0
- alayaflow/workflow/runnable/base_runnable_workflow.py +19 -0
- alayaflow/workflow/runnable/state_graph_runnable_workflow.py +23 -0
- alayaflow/workflow/workflow_info.py +50 -0
- alayaflow/workflow/workflow_loader.py +168 -0
- alayaflow/workflow/workflow_manager.py +257 -0
- alayaflow-0.1.0.dist-info/METADATA +99 -0
- alayaflow-0.1.0.dist-info/RECORD +37 -0
- alayaflow-0.1.0.dist-info/WHEEL +4 -0
- alayaflow-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from urllib import request
|
|
2
|
+
from alayaflow.clients.alayamem.http_client import HttpAlayaMemClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RetrieveComponent:
|
|
6
|
+
def __init__(self, client: HttpAlayaMemClient):
|
|
7
|
+
self.client = client
|
|
8
|
+
|
|
9
|
+
def __call__(self, query: str, collection_name: str, limit: int = 3) -> list[str]:
|
|
10
|
+
result = self.client.vdb_query([query], limit, collection_name)
|
|
11
|
+
return result.get('documents', [[]])[0] if result.get('documents') else []
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
client = HttpAlayaMemClient("http://10.16.70.46:5555")
|
|
15
|
+
res = client.vdb_query(messages="姓名", limit=5, collection_name="file_watcher_collection")
|
|
16
|
+
|
|
17
|
+
print(res)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from platform import java_ver
|
|
2
|
+
from langgraph.graph import StateGraph
|
|
3
|
+
from langgraph.graph.state import RunnableConfig
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
import requests
|
|
6
|
+
from typing import Annotated, Type, TypedDict, Optional
|
|
7
|
+
# from langchain_core.annotations import InjectedToolArg
|
|
8
|
+
|
|
9
|
+
from langchain_openai import ChatOpenAI
|
|
10
|
+
|
|
11
|
+
from alayaflow.common.config import settings
|
|
12
|
+
|
|
13
|
+
from langgraph.graph import START, END
|
|
14
|
+
from langchain.tools import tool
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
结点参数
|
|
18
|
+
- 每个结点使用的时候有固定的参数
|
|
19
|
+
- 1. 工作流定义中传入
|
|
20
|
+
- 包含结点的工作流创建的时候有全局变量,来配置工作流
|
|
21
|
+
- 2. 工作流创建时输入的全局变量
|
|
22
|
+
- 结点运行时有输入参数
|
|
23
|
+
- 3. 工作流运行时输入 (来自用户)
|
|
24
|
+
- 4. 运行时上游结点的输出
|
|
25
|
+
|
|
26
|
+
调用方式
|
|
27
|
+
- 放入langgraph中作单独结点
|
|
28
|
+
- 放入langflow component实现中
|
|
29
|
+
- ~放入LLM结点作tools~
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Core
|
|
33
|
+
|
|
34
|
+
def search(query, url, item_cnt, api_key):
|
|
35
|
+
# return requests.get(
|
|
36
|
+
# f"{url}/search",
|
|
37
|
+
# headers={"Authorization": f"Bearer {search_api_key}"},
|
|
38
|
+
# params={"query": query, "item": return_item_cnt}
|
|
39
|
+
# ).json()
|
|
40
|
+
|
|
41
|
+
# Fake run
|
|
42
|
+
return f"Get request from {url} with headers={{'Authorization': 'Bearer {api_key}'}} and params={{'query': {query}, 'item': {item_cnt}}}"
|
|
43
|
+
|
|
44
|
+
# @tool
|
|
45
|
+
# def search_as_tool(
|
|
46
|
+
# query: str,
|
|
47
|
+
# # 告诉 LangChain:这些参数不要给 LLM 看,由代码注入
|
|
48
|
+
# config: Annotated[dict, InjectedToolArg]
|
|
49
|
+
# ):
|
|
50
|
+
# """搜索工具,用于获取特定信息。"""
|
|
51
|
+
# # 从注入的 config 中提取配置
|
|
52
|
+
# return search(
|
|
53
|
+
# query=query,
|
|
54
|
+
# url=config["url"],
|
|
55
|
+
# api_key=config["api_key"],
|
|
56
|
+
# item_cnt=config.get("item_cnt", 3)
|
|
57
|
+
# )
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Workflow developer layer
|
|
61
|
+
|
|
62
|
+
class MyState(BaseModel):
|
|
63
|
+
query: str
|
|
64
|
+
search_result: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
# class SearchNode:
|
|
67
|
+
# def __init__(self, key: str):
|
|
68
|
+
# self.key = key
|
|
69
|
+
|
|
70
|
+
# def __call__(self, state: MyState):
|
|
71
|
+
# new_state = state.model_copy()
|
|
72
|
+
# new_state.search_result = search(
|
|
73
|
+
# state.query,
|
|
74
|
+
# "https://xxx", # 1
|
|
75
|
+
# 3, # 1
|
|
76
|
+
# self.key, # 2
|
|
77
|
+
# )
|
|
78
|
+
# return new_state
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_search_node(api_key: str):
|
|
82
|
+
|
|
83
|
+
def search_node(state: MyState, config: RunnableConfig):
|
|
84
|
+
result = search(
|
|
85
|
+
query=state.query,
|
|
86
|
+
url=config["configurable"]["search_url"],
|
|
87
|
+
api_key=config["configurable"]["search_api_key"],
|
|
88
|
+
item_cnt=3,
|
|
89
|
+
)
|
|
90
|
+
return {"search_result": result}
|
|
91
|
+
return search_node
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_graph(search_api_key):
|
|
95
|
+
builder = StateGraph(MyState)
|
|
96
|
+
|
|
97
|
+
# 添加节点
|
|
98
|
+
builder.add_node("search", create_search_node(search_api_key))
|
|
99
|
+
|
|
100
|
+
# 定义边
|
|
101
|
+
builder.add_edge(START, "search")
|
|
102
|
+
builder.add_edge("search", END)
|
|
103
|
+
|
|
104
|
+
return builder.compile()
|
|
105
|
+
|
|
106
|
+
class WFConfig(TypedDict):
|
|
107
|
+
search_api_key: str
|
|
108
|
+
search_url: str
|
|
109
|
+
|
|
110
|
+
def get_config_schema() -> Type[TypedDict]:
|
|
111
|
+
return WFConfig
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
graph = create_graph(search_api_key="xxx") # 2
|
|
115
|
+
result = graph.invoke({
|
|
116
|
+
"query": "唐博", # 3
|
|
117
|
+
}, config={
|
|
118
|
+
"configurable": WFConfig(
|
|
119
|
+
search_api_key="abc",
|
|
120
|
+
search_url="https://xxxx",
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
print(f"result: {result}")
|
|
126
|
+
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import hashlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Optional, Set, Tuple
|
|
9
|
+
|
|
10
|
+
from alayaflow.common.config import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvManager:
|
|
14
|
+
"""
|
|
15
|
+
使用 uv (Rust-based tool) 替代 venv + pip。
|
|
16
|
+
提供极速的环境创建和 Python 版本管理。
|
|
17
|
+
增强功能:根据requirements自动匹配现有环境
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.base_dir = settings.envs_dir
|
|
22
|
+
if not os.path.exists(self.base_dir):
|
|
23
|
+
os.makedirs(self.base_dir)
|
|
24
|
+
|
|
25
|
+
# 检查 uv 是否存在
|
|
26
|
+
if shutil.which("uv") is None:
|
|
27
|
+
print("[Warning] 'uv' command not found! Please install uv first.")
|
|
28
|
+
# 在实际桌面应用中,你应该将 uv 的二进制文件打包在你的应用里,
|
|
29
|
+
# 并在这里指定绝对路径,例如: self.uv_bin = "./bin/uv.exe"
|
|
30
|
+
self.uv_bin = "uv"
|
|
31
|
+
else:
|
|
32
|
+
self.uv_bin = "uv"
|
|
33
|
+
|
|
34
|
+
# 元数据存储
|
|
35
|
+
self.metadata_file = os.path.join(self.base_dir, "envs_metadata.json")
|
|
36
|
+
self.env_metadata = self._load_metadata()
|
|
37
|
+
|
|
38
|
+
# 初始化时扫描所有现有环境并更新包信息
|
|
39
|
+
self._scan_existing_envs()
|
|
40
|
+
|
|
41
|
+
def _load_metadata(self) -> Dict:
|
|
42
|
+
"""加载环境元数据"""
|
|
43
|
+
if os.path.exists(self.metadata_file):
|
|
44
|
+
try:
|
|
45
|
+
with open(self.metadata_file, 'r') as f:
|
|
46
|
+
return json.load(f)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
return {}
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
def _save_metadata(self):
|
|
52
|
+
"""保存环境元数据"""
|
|
53
|
+
with open(self.metadata_file, 'w') as f:
|
|
54
|
+
json.dump(self.env_metadata, f, indent=2)
|
|
55
|
+
|
|
56
|
+
# def get_venv_path(self, workflow_id: str, version: str) -> str:
|
|
57
|
+
# return os.path.join(self.base_dir, f"{workflow_id}_{version}")
|
|
58
|
+
|
|
59
|
+
def get_venv_path_by_name(self, env_name: str) -> str:
|
|
60
|
+
return os.path.join(self.base_dir, env_name)
|
|
61
|
+
|
|
62
|
+
def get_python_executable(self, env_path: str) -> str:
|
|
63
|
+
# uv 创建的 venv 结构和标准 venv 一样
|
|
64
|
+
if sys.platform == "win32":
|
|
65
|
+
return os.path.join(env_path, "Scripts", "python.exe")
|
|
66
|
+
return os.path.join(env_path, "bin", "python")
|
|
67
|
+
|
|
68
|
+
def _get_python_version(self, env_path: str) -> str:
|
|
69
|
+
"""使用subprocess调用python print获取环境的Python版本"""
|
|
70
|
+
python_exe = self.get_python_executable(env_path)
|
|
71
|
+
try:
|
|
72
|
+
cmd = [python_exe, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]
|
|
73
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
74
|
+
return result.stdout.strip()
|
|
75
|
+
except:
|
|
76
|
+
return "unknown"
|
|
77
|
+
|
|
78
|
+
def _scan_existing_envs(self):
|
|
79
|
+
"""扫描现有环境(子目录存在且有python_exe)并更新包信息"""
|
|
80
|
+
print("[UV] Scanning existing environments...")
|
|
81
|
+
for env_dir in os.listdir(self.base_dir):
|
|
82
|
+
# 跳过元数据文件
|
|
83
|
+
if env_dir == "envs_metadata.json":
|
|
84
|
+
continue
|
|
85
|
+
env_path = os.path.join(self.base_dir, env_dir)
|
|
86
|
+
# todo: consider if the following logic is clear. Seems not good
|
|
87
|
+
if os.path.isdir(env_path):
|
|
88
|
+
# 检查是否是有效的虚拟环境
|
|
89
|
+
python_exe = self.get_python_executable(env_path)
|
|
90
|
+
if os.path.exists(python_exe):
|
|
91
|
+
if env_dir not in self.env_metadata:
|
|
92
|
+
# 新发现的环境,生成包信息
|
|
93
|
+
print(f"[UV] Found new environment: {env_dir}")
|
|
94
|
+
self._update_env_packages(env_dir)
|
|
95
|
+
self._save_metadata()
|
|
96
|
+
|
|
97
|
+
def _update_env_packages(self, env_name: str):
|
|
98
|
+
"""更新指定环境的包信息"""
|
|
99
|
+
env_path = self.get_venv_path_by_name(env_name)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# 获取已安装的包列表
|
|
103
|
+
cmd = [self.uv_bin, "pip", "freeze", "-p", env_path]
|
|
104
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
105
|
+
|
|
106
|
+
packages = {}
|
|
107
|
+
for line in result.stdout.strip().split('\n'):
|
|
108
|
+
if line and '==' in line:
|
|
109
|
+
name, version = line.split('==', 1)
|
|
110
|
+
packages[name] = version
|
|
111
|
+
|
|
112
|
+
# 更新元数据
|
|
113
|
+
self.env_metadata[env_name] = {
|
|
114
|
+
"path": env_path,
|
|
115
|
+
"packages": packages,
|
|
116
|
+
"python_version": self._get_python_version(env_path),
|
|
117
|
+
# "requirements": [] # 初始安装的requirements(可选的)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
print(f"[UV] Updated packages for {env_name}: {len(packages)} packages")
|
|
121
|
+
|
|
122
|
+
except subprocess.CalledProcessError as e:
|
|
123
|
+
print(f"[UV] Failed to scan packages for {env_name}: {e}")
|
|
124
|
+
|
|
125
|
+
def _parse_requirement(self, req_str: str) -> Tuple[str, str]:
|
|
126
|
+
"""解析单个requirement字符串,返回包名和版本约束"""
|
|
127
|
+
# 移除注释和空格
|
|
128
|
+
req_str = req_str.split('#')[0].strip()
|
|
129
|
+
if not req_str:
|
|
130
|
+
return None, None
|
|
131
|
+
|
|
132
|
+
# 解析包名和版本
|
|
133
|
+
import re
|
|
134
|
+
# 匹配包名(允许字母、数字、点、下划线、连字符)
|
|
135
|
+
name_match = re.match(r'^([a-zA-Z0-9._-]+)', req_str)
|
|
136
|
+
if not name_match:
|
|
137
|
+
return None, None
|
|
138
|
+
|
|
139
|
+
name = name_match.group(1).lower()
|
|
140
|
+
version_spec = req_str[len(name):].strip()
|
|
141
|
+
|
|
142
|
+
# 标准化版本约束
|
|
143
|
+
if version_spec:
|
|
144
|
+
# 移除多余的空格
|
|
145
|
+
version_spec = version_spec.replace(' ', '')
|
|
146
|
+
else:
|
|
147
|
+
version_spec = "any" # 未指定版本
|
|
148
|
+
|
|
149
|
+
return name, version_spec
|
|
150
|
+
|
|
151
|
+
def _check_package_compatibility(self, installed_ver: str, required_spec: str) -> bool:
|
|
152
|
+
"""检查已安装版本是否满足要求"""
|
|
153
|
+
if required_spec == "any":
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
from packaging.version import parse, Version
|
|
157
|
+
from packaging.specifiers import SpecifierSet
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# 解析已安装版本
|
|
161
|
+
installed = parse(installed_ver)
|
|
162
|
+
if not isinstance(installed, Version):
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
# 解析版本要求
|
|
166
|
+
specifier = SpecifierSet(required_spec)
|
|
167
|
+
return installed in specifier
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"[EM] Version check error: {e}")
|
|
171
|
+
return False
|
|
172
|
+
# 如果解析失败,进行字符串比较作为后备
|
|
173
|
+
# try:
|
|
174
|
+
# # 简单的比较逻辑
|
|
175
|
+
# if required_spec.startswith("=="):
|
|
176
|
+
# return installed_ver == required_spec[2:]
|
|
177
|
+
# elif required_spec.startswith(">="):
|
|
178
|
+
# return installed_ver >= required_spec[2:]
|
|
179
|
+
# elif required_spec.startswith("<="):
|
|
180
|
+
# return installed_ver <= required_spec[2:]
|
|
181
|
+
# elif required_spec.startswith(">"):
|
|
182
|
+
# return installed_ver > required_spec[1:]
|
|
183
|
+
# elif required_spec.startswith("<"):
|
|
184
|
+
# return installed_ver < required_spec[1:]
|
|
185
|
+
# elif required_spec.startswith("~="):
|
|
186
|
+
# # 兼容版本:近似等于
|
|
187
|
+
# base_version = parse(required_spec[2:])
|
|
188
|
+
# installed_version = parse(installed_ver)
|
|
189
|
+
# return (installed_version >= base_version and
|
|
190
|
+
# installed_version < base_version.next_minor())
|
|
191
|
+
# except:
|
|
192
|
+
# return False
|
|
193
|
+
#
|
|
194
|
+
# return True
|
|
195
|
+
|
|
196
|
+
def find_matching_env(self, requirements: List[str], python_version: str = None) -> Optional[str]:
|
|
197
|
+
"""
|
|
198
|
+
查找满足requirements的现有环境
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
requirements: 包要求列表
|
|
202
|
+
python_version: 可选的Python版本要求
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
匹配的环境名称,或None
|
|
206
|
+
"""
|
|
207
|
+
if not requirements:
|
|
208
|
+
# 如果没有要求,返回第一个环境
|
|
209
|
+
return next(iter(self.env_metadata.keys()), None)
|
|
210
|
+
|
|
211
|
+
print(f"[EM] Looking for environment matching {len(requirements)} requirements...")
|
|
212
|
+
|
|
213
|
+
for env_name, metadata in self.env_metadata.items():
|
|
214
|
+
# 检查Python版本
|
|
215
|
+
if python_version and metadata.get("python_version") != python_version:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
env_packages = metadata.get("packages", {})
|
|
219
|
+
all_satisfied = True
|
|
220
|
+
|
|
221
|
+
for req in requirements:
|
|
222
|
+
if not req or req.startswith("#"):
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
pkg_name, version_spec = self._parse_requirement(req)
|
|
226
|
+
if not pkg_name:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# 检查包是否安装
|
|
230
|
+
if pkg_name not in env_packages:
|
|
231
|
+
all_satisfied = False
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
# 检查版本是否满足
|
|
235
|
+
installed_ver = env_packages[pkg_name]
|
|
236
|
+
if not self._check_package_compatibility(installed_ver, version_spec):
|
|
237
|
+
all_satisfied = False
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
if all_satisfied:
|
|
241
|
+
print(f"[EM] Found matching environment: {env_name}")
|
|
242
|
+
return env_name
|
|
243
|
+
|
|
244
|
+
print("[EM] No matching environment found")
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def create_new_env(self, env_name: str, python_version: str = "3.12") -> str:
|
|
248
|
+
"""创建新的虚拟环境"""
|
|
249
|
+
env_path = self.get_venv_path_by_name(env_name)
|
|
250
|
+
python_exe = self.get_python_executable(env_path)
|
|
251
|
+
|
|
252
|
+
if not os.path.exists(python_exe):
|
|
253
|
+
print(f"[UV] Creating new VENV: {env_name}...")
|
|
254
|
+
try:
|
|
255
|
+
subprocess.check_call(
|
|
256
|
+
[self.uv_bin, "venv", env_path, "--python", python_version],
|
|
257
|
+
stdout=subprocess.DEVNULL
|
|
258
|
+
)
|
|
259
|
+
print(f"[UV] Venv created with Python {python_version}")
|
|
260
|
+
except subprocess.CalledProcessError as e:
|
|
261
|
+
print(f"[UV] Venv creation failed: {e}")
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
return env_path
|
|
265
|
+
|
|
266
|
+
def ensure_env(self, workflow_id: str, version: str, requirements: List[str], use_venv: bool) -> str:
|
|
267
|
+
"""
|
|
268
|
+
使用 uv 准备环境
|
|
269
|
+
先尝试匹配现有环境,如无匹配则创建新环境
|
|
270
|
+
"""
|
|
271
|
+
if not use_venv:
|
|
272
|
+
return sys.executable
|
|
273
|
+
|
|
274
|
+
# 1. 首先尝试查找匹配的现有环境
|
|
275
|
+
env_name = self.find_matching_env(requirements)
|
|
276
|
+
|
|
277
|
+
if env_name:
|
|
278
|
+
# 使用现有环境
|
|
279
|
+
env_path = self.get_venv_path_by_name(env_name)
|
|
280
|
+
python_exe = self.get_python_executable(env_path)
|
|
281
|
+
|
|
282
|
+
print(f"[EM] Reusing existing environment: {env_name}")
|
|
283
|
+
|
|
284
|
+
# 检查是否安装了alayaflow
|
|
285
|
+
alayaflow_marker = os.path.join(env_path, "alayaflow_installed.marker")
|
|
286
|
+
if not os.path.exists(alayaflow_marker):
|
|
287
|
+
self._install_alayaflow(env_path)
|
|
288
|
+
|
|
289
|
+
return python_exe
|
|
290
|
+
|
|
291
|
+
# 2. 没有匹配的环境,创建新环境
|
|
292
|
+
import uuid
|
|
293
|
+
# todo: use function create_new_env instead?
|
|
294
|
+
# env_name = f"{workflow_id}_{version}"
|
|
295
|
+
# env_path = self.get_venv_path(workflow_id, version)
|
|
296
|
+
env_name=uuid.uuid4().hex
|
|
297
|
+
env_path = self.get_venv_path_by_name(env_name)
|
|
298
|
+
python_exe = self.get_python_executable(env_path)
|
|
299
|
+
|
|
300
|
+
# 1. 创建虚拟环境 (uv venv)
|
|
301
|
+
# 优势:如果指定了 python 版本,uv 会自动下载便携版,不需要系统预装
|
|
302
|
+
if not os.path.exists(python_exe):
|
|
303
|
+
print(f"[UV] Creating VENV for {workflow_id} v{version}...")
|
|
304
|
+
try:
|
|
305
|
+
# 假设我们想让这个环境使用 Python 3.12 (即使主程序是 3.9)
|
|
306
|
+
# 实际中你可以把 python_version 放在元数据里传进来
|
|
307
|
+
target_python = "3.12"
|
|
308
|
+
|
|
309
|
+
subprocess.check_call(
|
|
310
|
+
[self.uv_bin, "venv", env_path, "--python", target_python],
|
|
311
|
+
stdout=subprocess.DEVNULL
|
|
312
|
+
)
|
|
313
|
+
print(f"[UV] Venv created with Python {target_python}")
|
|
314
|
+
except subprocess.CalledProcessError as e:
|
|
315
|
+
print(f"[UV] Venv creation failed: {e}")
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
# 2. 安装依赖 (uv pip install)
|
|
319
|
+
if requirements:
|
|
320
|
+
self._install_requirements(env_path, requirements)
|
|
321
|
+
|
|
322
|
+
# 安装 alayaflow
|
|
323
|
+
self._install_alayaflow(env_path)
|
|
324
|
+
|
|
325
|
+
# 更新环境元数据
|
|
326
|
+
self._update_env_packages(env_name)
|
|
327
|
+
|
|
328
|
+
return python_exe
|
|
329
|
+
|
|
330
|
+
def _install_requirements(self, env_path: str, requirements: List[str]):
|
|
331
|
+
"""安装依赖"""
|
|
332
|
+
marker_file = os.path.join(env_path, "deps_installed.marker")
|
|
333
|
+
if not os.path.exists(marker_file):
|
|
334
|
+
print(f"[UV] Installing deps: {requirements}")
|
|
335
|
+
try:
|
|
336
|
+
cmd = [self.uv_bin, "pip", "install", "-p", env_path] + requirements
|
|
337
|
+
|
|
338
|
+
subprocess.check_call(
|
|
339
|
+
cmd,
|
|
340
|
+
stdout=subprocess.DEVNULL,
|
|
341
|
+
stderr=subprocess.PIPE
|
|
342
|
+
)
|
|
343
|
+
with open(marker_file, "w") as f:
|
|
344
|
+
f.write("ok")
|
|
345
|
+
print("[UV] Deps installed.")
|
|
346
|
+
|
|
347
|
+
except subprocess.CalledProcessError as e:
|
|
348
|
+
print(f"[UV] Install failed: {e}")
|
|
349
|
+
raise
|
|
350
|
+
|
|
351
|
+
def _install_alayaflow(self, env_path: str):
|
|
352
|
+
"""安装alayaflow"""
|
|
353
|
+
# todo: Why this appear?
|
|
354
|
+
alayaflow_marker = os.path.join(env_path, "alayaflow_installed.marker")
|
|
355
|
+
if not os.path.exists(alayaflow_marker):
|
|
356
|
+
print("[UV] Installing alayaflow in worker env...")
|
|
357
|
+
try:
|
|
358
|
+
if settings.is_dev():
|
|
359
|
+
# dev模式:可编辑安装
|
|
360
|
+
subprocess.check_call(
|
|
361
|
+
[self.uv_bin, "pip", "install", "-p", env_path, "-e", str(settings.alayaflow_root)]
|
|
362
|
+
)
|
|
363
|
+
elif settings.is_uneditable_dev():
|
|
364
|
+
# dev-uneditable:从项目根目录安装(非可编辑模式)
|
|
365
|
+
print(f"[UV] Installing alayaflow from: {settings.alayaflow_root}")
|
|
366
|
+
subprocess.check_call(
|
|
367
|
+
[self.uv_bin, "pip", "install", "-p", env_path, str(settings.alayaflow_root)],
|
|
368
|
+
stdout=subprocess.DEVNULL,
|
|
369
|
+
stderr=subprocess.PIPE,
|
|
370
|
+
)
|
|
371
|
+
elif settings.is_prod():
|
|
372
|
+
# prod模式:从PyPI安装
|
|
373
|
+
subprocess.check_call(
|
|
374
|
+
[self.uv_bin, "pip", "install", "-p", env_path, "alayaflow"],
|
|
375
|
+
stdout=subprocess.DEVNULL,
|
|
376
|
+
stderr=subprocess.PIPE
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
raise ValueError(f"Unknown dev_mode: {settings.dev_mode}")
|
|
380
|
+
with open(alayaflow_marker, "w") as f:
|
|
381
|
+
f.write("ok")
|
|
382
|
+
print("[UV] Alayaflow installed.")
|
|
383
|
+
except subprocess.CalledProcessError as e:
|
|
384
|
+
print(f"[UV] Alayaflow installation failed: {e}")
|
|
385
|
+
raise
|
|
386
|
+
|
|
387
|
+
def list_environments(self) -> List[Dict]:
|
|
388
|
+
"""列出所有环境及其信息"""
|
|
389
|
+
envs = []
|
|
390
|
+
for env_name, metadata in self.env_metadata.items():
|
|
391
|
+
env_info = {
|
|
392
|
+
"name": env_name,
|
|
393
|
+
"path": metadata.get("path", ""),
|
|
394
|
+
"python_version": metadata.get("python_version", "unknown"),
|
|
395
|
+
"package_count": len(metadata.get("packages", {})),
|
|
396
|
+
}
|
|
397
|
+
envs.append(env_info)
|
|
398
|
+
return envs
|
|
399
|
+
|
|
400
|
+
# def clean_orphaned_envs(self):
|
|
401
|
+
# # 不想开放这个功能
|
|
402
|
+
# """清理元数据中不存在的环境"""
|
|
403
|
+
# env_dirs = os.listdir(self.base_dir)
|
|
404
|
+
#
|
|
405
|
+
# # 找出元数据中有但实际不存在的环境
|
|
406
|
+
# to_remove = []
|
|
407
|
+
# for env_name in list(self.env_metadata.keys()):
|
|
408
|
+
# env_path = self.get_venv_path_by_name(env_name)
|
|
409
|
+
# if not os.path.exists(env_path):
|
|
410
|
+
# to_remove.append(env_name)
|
|
411
|
+
#
|
|
412
|
+
# # 清理元数据
|
|
413
|
+
# for env_name in to_remove:
|
|
414
|
+
# # del self.env_metadata[env_name]
|
|
415
|
+
# print(f"[UV] Remove orphaned metadata for {env_name}")
|
|
416
|
+
#
|
|
417
|
+
# if to_remove:
|
|
418
|
+
# self._save_metadata()
|
|
419
|
+
|
|
420
|
+
def get_env_info(self, env_name: str) -> Optional[Dict]:
|
|
421
|
+
"""获取环境的详细信息"""
|
|
422
|
+
if env_name in self.env_metadata:
|
|
423
|
+
return self.env_metadata[env_name]
|
|
424
|
+
return None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Generator, Dict, Optional
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
5
|
+
from alayaflow.execution.executors.uv_executor import UvExecutor
|
|
6
|
+
from alayaflow.execution.executors.naive_executor import NaiveExecutor
|
|
7
|
+
from alayaflow.execution.executors.worker_executor import WorkerExecutor
|
|
8
|
+
from alayaflow.workflow import WorkflowManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExecutorType(Enum):
|
|
12
|
+
UV = "uv"
|
|
13
|
+
NAIVE = "naive"
|
|
14
|
+
WORKER = "worker"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecutorManager:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
workflow_manager: WorkflowManager
|
|
21
|
+
):
|
|
22
|
+
self._workflow_manager = workflow_manager
|
|
23
|
+
self._executor_map: Dict[ExecutorType, BaseExecutor] = {}
|
|
24
|
+
self._initialize_executors()
|
|
25
|
+
|
|
26
|
+
def _initialize_executors(self):
|
|
27
|
+
"""Initialize all available executors."""
|
|
28
|
+
self._executor_map[ExecutorType.UV] = UvExecutor(
|
|
29
|
+
workflow_manager=self._workflow_manager
|
|
30
|
+
)
|
|
31
|
+
self._executor_map[ExecutorType.NAIVE] = NaiveExecutor(
|
|
32
|
+
workflow_manager=self._workflow_manager
|
|
33
|
+
)
|
|
34
|
+
self._executor_map[ExecutorType.WORKER] = WorkerExecutor(
|
|
35
|
+
workflow_manager=self._workflow_manager
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def init_workflow(self, workflow_id: str, version: str, init_args: dict):
|
|
39
|
+
for executor in self._executor_map.values():
|
|
40
|
+
executor.init_workflow(workflow_id, version, init_args)
|
|
41
|
+
|
|
42
|
+
def exec_workflow(
|
|
43
|
+
self,
|
|
44
|
+
workflow_id: str,
|
|
45
|
+
version: str,
|
|
46
|
+
input_data: dict,
|
|
47
|
+
user_config: dict,
|
|
48
|
+
executor_type: ExecutorType | str = ExecutorType.NAIVE
|
|
49
|
+
) -> Generator[Dict, None, None]:
|
|
50
|
+
if isinstance(executor_type, str):
|
|
51
|
+
executor_type = ExecutorType(executor_type)
|
|
52
|
+
if executor_type not in self._executor_map:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Unsupported executor kind: {executor_type}. "
|
|
55
|
+
f"Supported kinds: {list(self._executor_map.keys())}"
|
|
56
|
+
)
|
|
57
|
+
executor = self._executor_map[executor_type]
|
|
58
|
+
yield from executor.execute_stream(workflow_id, version, input_data, user_config)
|
|
59
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Executor implementations for workflow execution."""
|
|
2
|
+
|
|
3
|
+
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
4
|
+
from alayaflow.execution.executors.uv_executor import UvExecutor
|
|
5
|
+
from alayaflow.execution.executors.naive_executor import NaiveExecutor
|
|
6
|
+
from alayaflow.execution.executors.worker_executor import WorkerExecutor
|
|
7
|
+
|
|
8
|
+
__all__ = ["BaseExecutor", "UvExecutor", "NaiveExecutor", "WorkerExecutor"]
|
|
9
|
+
|