alayaflow 0.1.2__py3-none-any.whl → 0.1.4__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 +4 -2
- alayaflow/api/api_singleton.py +27 -2
- alayaflow/clients/alayamem/base_client.py +16 -4
- alayaflow/clients/alayamem/http_client.py +60 -37
- alayaflow/common/config.py +10 -8
- alayaflow/component/llm_node.py +4 -5
- alayaflow/component/memory.py +28 -26
- alayaflow/component/retrieve_node.py +68 -2
- alayaflow/execution/executor_manager.py +19 -1
- alayaflow/execution/executors/base_executor.py +5 -1
- alayaflow/execution/executors/naive_executor.py +18 -56
- alayaflow/execution/executors/uv_executor.py +10 -1
- alayaflow/execution/executors/worker_executor.py +3 -1
- alayaflow/utils/coroutine.py +20 -0
- alayaflow/utils/logger.py +79 -0
- {alayaflow-0.1.2.dist-info → alayaflow-0.1.4.dist-info}/METADATA +1 -1
- {alayaflow-0.1.2.dist-info → alayaflow-0.1.4.dist-info}/RECORD +19 -17
- {alayaflow-0.1.2.dist-info → alayaflow-0.1.4.dist-info}/WHEEL +0 -0
- {alayaflow-0.1.2.dist-info → alayaflow-0.1.4.dist-info}/licenses/LICENSE +0 -0
alayaflow/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"""AlayaFlow - A
|
|
1
|
+
"""AlayaFlow - A workflow engen for management and execution."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
__version__ = version("alayaflow")
|
|
3
6
|
|
|
4
|
-
__version__ = "0.1.2"
|
|
5
7
|
|
alayaflow/api/api_singleton.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Generator, Dict, List, Self, Optional
|
|
1
|
+
from typing import Generator, Dict, List, Self, Optional, AsyncGenerator
|
|
2
2
|
|
|
3
3
|
from alayaflow.utils.singleton import SingletonMeta
|
|
4
4
|
from alayaflow.workflow import WorkflowManager
|
|
@@ -6,6 +6,10 @@ from alayaflow.execution import ExecutorManager, ExecutorType
|
|
|
6
6
|
from alayaflow.common.config import settings
|
|
7
7
|
from alayaflow.component.model import ModelManager, ModelProfile
|
|
8
8
|
from alayaflow.workflow.runnable import BaseRunnableWorkflow
|
|
9
|
+
from alayaflow.utils.logger import AlayaFlowLogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = AlayaFlowLogger()
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
class APISingleton(metaclass=SingletonMeta):
|
|
@@ -15,7 +19,7 @@ class APISingleton(metaclass=SingletonMeta):
|
|
|
15
19
|
workflow_manager=self.workflow_manager
|
|
16
20
|
)
|
|
17
21
|
self.model_manager = ModelManager()
|
|
18
|
-
self._inited = False
|
|
22
|
+
self._inited = False
|
|
19
23
|
|
|
20
24
|
def is_inited(self) -> bool:
|
|
21
25
|
return self._inited
|
|
@@ -34,6 +38,8 @@ class APISingleton(metaclass=SingletonMeta):
|
|
|
34
38
|
settings.langfuse_public_key = config.get("langfuse_public_key", settings.langfuse_public_key)
|
|
35
39
|
settings.langfuse_secret_key = config.get("langfuse_secret_key", settings.langfuse_secret_key)
|
|
36
40
|
settings.langfuse_url = config.get("langfuse_url", settings.langfuse_url)
|
|
41
|
+
|
|
42
|
+
logger.info(f"AlayaFlow is initialized with config: {settings.model_dump()}")
|
|
37
43
|
|
|
38
44
|
self._inited = True
|
|
39
45
|
return self
|
|
@@ -97,3 +103,22 @@ class APISingleton(metaclass=SingletonMeta):
|
|
|
97
103
|
executor_type=executor_type
|
|
98
104
|
):
|
|
99
105
|
yield event
|
|
106
|
+
|
|
107
|
+
async def exec_workflow_async(
|
|
108
|
+
self,
|
|
109
|
+
workflow_id: str,
|
|
110
|
+
version: str,
|
|
111
|
+
inputs: dict,
|
|
112
|
+
context: dict,
|
|
113
|
+
executor_type: str | ExecutorType = ExecutorType.NAIVE
|
|
114
|
+
) -> AsyncGenerator[dict, None]:
|
|
115
|
+
"""异步执行工作流"""
|
|
116
|
+
self._check_init()
|
|
117
|
+
async for event in self.executor_manager.exec_workflow_async(
|
|
118
|
+
workflow_id=workflow_id,
|
|
119
|
+
version=version,
|
|
120
|
+
inputs=inputs,
|
|
121
|
+
context=context,
|
|
122
|
+
executor_type=executor_type
|
|
123
|
+
):
|
|
124
|
+
yield event
|
|
@@ -2,18 +2,30 @@ from abc import ABC, abstractmethod
|
|
|
2
2
|
from typing import Dict, List, Any
|
|
3
3
|
|
|
4
4
|
class BaseAlayaMemClient(ABC):
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def query_file(self, user_id: str, message: Any, limit: int = 10, use_query_params: bool = False) -> Dict:
|
|
9
|
+
""" 查询文件内容 - POST 请求 """
|
|
10
|
+
pass
|
|
11
|
+
|
|
5
12
|
@abstractmethod
|
|
6
|
-
def
|
|
13
|
+
def commit_turn(self, session_id: str, user_text: str, assistant_text: str,
|
|
14
|
+
user_id: str = None, window_size: int = 10) -> Dict:
|
|
15
|
+
""" 提交多轮对话记录 - POST 请求 """
|
|
7
16
|
pass
|
|
8
17
|
|
|
9
18
|
@abstractmethod
|
|
10
|
-
def
|
|
19
|
+
def turns(self, session_id: str, user_id: str = None, limit: int = 10) -> List:
|
|
20
|
+
""" 查询对话历史记录 - GET 请求 """
|
|
11
21
|
pass
|
|
12
22
|
|
|
13
23
|
@abstractmethod
|
|
14
|
-
def
|
|
24
|
+
def summary(self, session_id: str, user_id: str = None) -> Dict:
|
|
25
|
+
""" 查询会话摘要 - GET 请求 """
|
|
15
26
|
pass
|
|
16
27
|
|
|
17
28
|
@abstractmethod
|
|
18
|
-
def
|
|
29
|
+
def profile(self, user_id: str) -> Dict:
|
|
30
|
+
""" 查询用户画像 - GET 请求 """
|
|
19
31
|
pass
|
|
@@ -5,60 +5,83 @@ from alayaflow.clients.alayamem.base_client import BaseAlayaMemClient
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class HttpAlayaMemClient(BaseAlayaMemClient):
|
|
8
|
+
|
|
8
9
|
def __init__(self, base_url: str):
|
|
9
|
-
|
|
10
|
+
"""Initialize the HTTP client with base URL"""
|
|
11
|
+
self.base_url = base_url.rstrip('/')
|
|
10
12
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
def query_file(self, user_id, message, limit=10, use_query_params=False):
|
|
14
|
+
""" 查询文件内容 - POST 请求 """
|
|
15
|
+
query_content = self._msg_content(message)
|
|
16
|
+
params = {
|
|
17
|
+
"query": query_content,
|
|
18
|
+
"user_id": user_id,
|
|
19
|
+
"limit": limit
|
|
20
|
+
}
|
|
21
|
+
return self._post_with_params("/memory/query_file", params)
|
|
22
|
+
|
|
17
23
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
24
|
+
def commit_turn(self, session_id, user_text, assistant_text, user_id=None, window_size=10):
|
|
25
|
+
""" 提交多轮对话记录 - POST 请求 """
|
|
26
|
+
payload = {
|
|
27
|
+
"session_id": session_id,
|
|
28
|
+
"user_text": user_text,
|
|
29
|
+
"assistant_text": assistant_text,
|
|
30
|
+
"window_size": window_size
|
|
31
|
+
}
|
|
32
|
+
if user_id:
|
|
33
|
+
payload["user_id"] = user_id
|
|
34
|
+
return self._post("/memory/commit_turn", payload)
|
|
25
35
|
|
|
26
|
-
def
|
|
27
|
-
|
|
36
|
+
def turns(self, session_id, user_id=None, limit=10):
|
|
37
|
+
""" 查询对话历史记录 - GET 请求 """
|
|
38
|
+
params = {
|
|
39
|
+
"session_id": session_id,
|
|
40
|
+
"limit": limit
|
|
41
|
+
}
|
|
42
|
+
if user_id:
|
|
43
|
+
params["user_id"] = user_id
|
|
44
|
+
return self._get("/memory/turns", params)
|
|
45
|
+
|
|
46
|
+
def summary(self, session_id, user_id=None):
|
|
47
|
+
""" 查询会话摘要 - GET 请求 """
|
|
48
|
+
params = {
|
|
49
|
+
"session_id": session_id,
|
|
50
|
+
}
|
|
51
|
+
if user_id:
|
|
52
|
+
params["user_id"] = user_id
|
|
53
|
+
return self._get("/memory/summary", params)
|
|
54
|
+
|
|
55
|
+
def profile(self, user_id):
|
|
56
|
+
""" 查询用户画像 - GET 请求 """
|
|
57
|
+
return self._get("/memory/profile", {
|
|
28
58
|
"user_id": user_id,
|
|
29
|
-
"question": self._msg_content(message),
|
|
30
59
|
})
|
|
31
60
|
|
|
32
|
-
def add_session_messages(self, user_id, messages):
|
|
33
|
-
for msg in messages:
|
|
34
|
-
return self._post("/add_message", {
|
|
35
|
-
"user_id": user_id,
|
|
36
|
-
"message": self._msg_content(msg),
|
|
37
|
-
"is_user": self._is_user(msg),
|
|
38
|
-
"speaker_name": "unknown",
|
|
39
|
-
})
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
|
|
63
|
+
# ---------- 私有辅助方法 ----------
|
|
64
|
+
def _get(self, path: str, params: dict):
|
|
65
|
+
"""发送 GET 请求,参数作为 query string"""
|
|
66
|
+
resp = requests.get(f"{self.base_url}{path}", params=params)
|
|
67
|
+
resp.raise_for_status()
|
|
68
|
+
return resp.json()
|
|
42
69
|
|
|
43
70
|
def _post(self, path: str, payload: dict):
|
|
71
|
+
"""发送 POST 请求,参数作为 JSON Body"""
|
|
44
72
|
resp = requests.post(f"{self.base_url}{path}", json=payload)
|
|
45
73
|
resp.raise_for_status()
|
|
46
74
|
return resp.json()
|
|
47
75
|
|
|
76
|
+
def _post_with_params(self, path: str, params: dict):
|
|
77
|
+
"""发送 POST 请求,参数作为 Query Parameters"""
|
|
78
|
+
resp = requests.post(f"{self.base_url}{path}", params=params)
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
return resp.json()
|
|
81
|
+
|
|
48
82
|
def _msg_content(self, msg):
|
|
49
83
|
# 支持字典格式
|
|
50
84
|
if isinstance(msg, dict):
|
|
51
85
|
return msg.get("content", str(msg))
|
|
52
86
|
# 支持对象格式
|
|
53
87
|
return msg.content if hasattr(msg, "content") else str(msg)
|
|
54
|
-
|
|
55
|
-
def _extract_query(self, messages):
|
|
56
|
-
if not messages:
|
|
57
|
-
return ""
|
|
58
|
-
last_msg = messages[-1]
|
|
59
|
-
return self._msg_content(last_msg)
|
|
60
|
-
|
|
61
|
-
def _is_user(self, msg):
|
|
62
|
-
if isinstance(msg, dict):
|
|
63
|
-
return msg.get("role", "").lower() == "user"
|
|
64
|
-
return "Human" in msg.__class__.__name__ or "User" in msg.__class__.__name__
|
alayaflow/common/config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Literal, Optional
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
from pydantic import model_validator, Field
|
|
4
5
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
6
|
|
|
6
7
|
class Settings(BaseSettings):
|
|
@@ -24,8 +25,9 @@ class Settings(BaseSettings):
|
|
|
24
25
|
workspace_root: Optional[Path] = None
|
|
25
26
|
resources_root: Optional[Path] = None
|
|
26
27
|
workflow_storage_path: Optional[Path] = None
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
30
|
+
log_dir: Optional[Path] = None
|
|
29
31
|
|
|
30
32
|
alayahub_url: str = "http://localhost:8000"
|
|
31
33
|
|
|
@@ -84,6 +86,11 @@ class Settings(BaseSettings):
|
|
|
84
86
|
else:
|
|
85
87
|
self.workflow_storage_path = Path(self.workflow_storage_path)
|
|
86
88
|
|
|
89
|
+
if self.log_dir is None:
|
|
90
|
+
self.log_dir = self.workspace_root / "logs"
|
|
91
|
+
else:
|
|
92
|
+
self.log_dir = Path(self.log_dir)
|
|
93
|
+
|
|
87
94
|
return self
|
|
88
95
|
|
|
89
96
|
@model_validator(mode='after')
|
|
@@ -98,9 +105,4 @@ class Settings(BaseSettings):
|
|
|
98
105
|
|
|
99
106
|
return self
|
|
100
107
|
|
|
101
|
-
@model_validator(mode='after')
|
|
102
|
-
def print_config(self):
|
|
103
|
-
print(self.model_dump())
|
|
104
|
-
return self
|
|
105
|
-
|
|
106
108
|
settings = Settings()
|
alayaflow/component/llm_node.py
CHANGED
|
@@ -18,17 +18,14 @@ class LLMComponent:
|
|
|
18
18
|
def __init__(
|
|
19
19
|
self,
|
|
20
20
|
*,
|
|
21
|
-
# ===== 模型 & prompt =====
|
|
22
21
|
model_id: str,
|
|
23
22
|
system_prompt: str,
|
|
24
23
|
prompt: str,
|
|
25
24
|
|
|
26
|
-
# ===== 采样参数 =====
|
|
27
25
|
temperature: Optional[float] = None,
|
|
28
26
|
top_p: Optional[float] = None,
|
|
29
27
|
max_tokens: Optional[int] = None,
|
|
30
28
|
|
|
31
|
-
# ===== 输出控制 =====
|
|
32
29
|
response_format: ResponseFormat = ResponseFormat.TEXT,
|
|
33
30
|
json_schema: Optional[Dict[str, Any]] = None,
|
|
34
31
|
outputs: Optional[Dict[str, str]] = None,
|
|
@@ -47,6 +44,9 @@ class LLMComponent:
|
|
|
47
44
|
self.json_schema = json_schema
|
|
48
45
|
self.outputs = outputs or {}
|
|
49
46
|
self.retry_json_once = retry_json_once
|
|
47
|
+
|
|
48
|
+
# —— 依赖注入(获取全局单例 ModelManager)——
|
|
49
|
+
self._model_manager = ModelManager()
|
|
50
50
|
|
|
51
51
|
def _get_llm(self) -> Runnable:
|
|
52
52
|
bind_kwargs: Dict[str, Any] = {}
|
|
@@ -58,8 +58,7 @@ class LLMComponent:
|
|
|
58
58
|
if self.max_tokens is not None:
|
|
59
59
|
bind_kwargs["max_tokens"] = self.max_tokens
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
llm = model_manager.get_model(self.model_id, runtime_config=bind_kwargs)
|
|
61
|
+
llm = self._model_manager.get_model(self.model_id, runtime_config=bind_kwargs)
|
|
63
62
|
|
|
64
63
|
return llm
|
|
65
64
|
|
alayaflow/component/memory.py
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
1
|
from alayaflow.clients.alayamem.http_client import HttpAlayaMemClient
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def
|
|
4
|
+
def query_file(alayamem_url: str, user_id: str, message):
|
|
5
5
|
mem_client = HttpAlayaMemClient(alayamem_url)
|
|
6
6
|
try:
|
|
7
|
-
|
|
8
|
-
return {"vdb_results": result.get("results", []), "vdb_response": result}
|
|
7
|
+
return mem_client.query_file(user_id, message)
|
|
9
8
|
except Exception as e:
|
|
10
|
-
|
|
9
|
+
print(f"[Query File] Error: {e}")
|
|
10
|
+
return {"error": str(e)}
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def commit_turn(alayamem_url: str, session_id: str, user_text: str,
|
|
14
|
+
assistant_text: str, user_id: str = None, window_size: int = 3):
|
|
15
|
+
""" 提交多轮对话记录 """
|
|
14
16
|
mem_client = HttpAlayaMemClient(alayamem_url)
|
|
15
17
|
try:
|
|
16
|
-
return mem_client.
|
|
18
|
+
return mem_client.commit_turn(session_id, user_text, assistant_text,
|
|
19
|
+
user_id, window_size)
|
|
17
20
|
except Exception as e:
|
|
18
|
-
|
|
21
|
+
print(f"[Commit Turn] Error: {e}")
|
|
22
|
+
return {"error": str(e)}
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
def
|
|
25
|
+
def turns(alayamem_url: str, session_id: str, user_id: str = None, limit: int = 10):
|
|
26
|
+
""" 查询对话历史记录 """
|
|
22
27
|
mem_client = HttpAlayaMemClient(alayamem_url)
|
|
28
|
+
try:
|
|
29
|
+
return mem_client.turns(session_id, user_id, limit)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"[Turns] Error: {e}")
|
|
32
|
+
return {"error": str(e)}
|
|
23
33
|
|
|
24
|
-
if not messages:
|
|
25
|
-
return
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
def summary(alayamem_url: str, session_id: str, user_id: str = None):
|
|
36
|
+
""" 查询会话摘要 """
|
|
37
|
+
mem_client = HttpAlayaMemClient(alayamem_url)
|
|
29
38
|
try:
|
|
30
|
-
|
|
39
|
+
return mem_client.summary(session_id, user_id)
|
|
31
40
|
except Exception as e:
|
|
32
|
-
print(f"[
|
|
33
|
-
return
|
|
41
|
+
print(f"[Summary] Error: {e}")
|
|
42
|
+
return {"error": str(e)}
|
|
34
43
|
|
|
35
|
-
return resp
|
|
36
44
|
|
|
37
45
|
|
|
38
|
-
def
|
|
46
|
+
def profile(alayamem_url: str, user_id: str):
|
|
39
47
|
mem_client = HttpAlayaMemClient(alayamem_url)
|
|
40
|
-
|
|
41
|
-
if not messages:
|
|
42
|
-
return
|
|
43
|
-
|
|
44
48
|
try:
|
|
45
|
-
return mem_client.
|
|
49
|
+
return mem_client.profile(user_id)
|
|
46
50
|
except Exception as e:
|
|
47
|
-
print(f"[
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
+
print(f"[Profile] Error: {e}")
|
|
52
|
+
return {"error": str(e)}
|
|
@@ -7,5 +7,71 @@ class RetrieveComponent:
|
|
|
7
7
|
self.client = client
|
|
8
8
|
|
|
9
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 []
|
|
10
|
+
# result = self.client.vdb_query([query], limit, collection_name)
|
|
11
|
+
# return result.get('documents', [[]])[0] if result.get('documents') else []
|
|
12
|
+
docs: list[str] = []
|
|
13
|
+
|
|
14
|
+
# -------- 基础个人信息 --------
|
|
15
|
+
if "姓名" in query:
|
|
16
|
+
docs.extend([
|
|
17
|
+
"用户姓名是张三。",
|
|
18
|
+
"张三,男,1989年出生。",
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
if "性别" in query:
|
|
22
|
+
docs.append("张三,男,工程师。")
|
|
23
|
+
|
|
24
|
+
if "年龄" in query:
|
|
25
|
+
docs.append("张三,1989年出生,今年34岁。")
|
|
26
|
+
|
|
27
|
+
# -------- 联系方式 --------
|
|
28
|
+
if "电话" in query:
|
|
29
|
+
docs.extend([
|
|
30
|
+
"联系电话:15643431212。",
|
|
31
|
+
"手机号为15643431212,可随时联系。",
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
if "邮箱" in query or "Email" in query:
|
|
35
|
+
docs.extend([
|
|
36
|
+
"电子邮箱:273230101@qq.com。",
|
|
37
|
+
"邮箱地址为 27223221@qq.com。",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
# -------- 工作单位 / 职务 --------
|
|
41
|
+
if "工作单位" in query or "公司" in query:
|
|
42
|
+
docs.extend([
|
|
43
|
+
"现就职于 Google 中国。",
|
|
44
|
+
"工作单位为 Google。",
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
if "职务" in query or "职位" in query:
|
|
48
|
+
docs.append("现任高级软件工程师。")
|
|
49
|
+
|
|
50
|
+
# -------- 教育背景 --------
|
|
51
|
+
if "学历" in query or "教育" in query:
|
|
52
|
+
docs.extend([
|
|
53
|
+
"2016年本科毕业于清华大学计算机科学与技术专业。",
|
|
54
|
+
"2020年博士毕业于北京大学。",
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
# -------- 工作 / 学习经历 --------
|
|
58
|
+
if "学习工作经历" in query or "经历" in query:
|
|
59
|
+
docs.extend([
|
|
60
|
+
"2010.09-2014.07 就读于清华大学计算机科学与技术专业。",
|
|
61
|
+
"2014.07-2016.08 在百度公司担任软件开发工程师。",
|
|
62
|
+
"2016.09 至今在 Google 担任高级软件工程师。",
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
# -------- 路径/父字段增强(模拟更真实检索)--------
|
|
66
|
+
if "个人信息" in query and not docs:
|
|
67
|
+
docs.append("张三,男,1989年出生,工程师,现居北京。")
|
|
68
|
+
|
|
69
|
+
if "联系方式" in query and not docs:
|
|
70
|
+
docs.append("联系方式:电话 15643431212,邮箱 272040101@qq.com。")
|
|
71
|
+
|
|
72
|
+
# -------- 兜底:模拟找不到 --------
|
|
73
|
+
if not docs:
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
# limit 截断
|
|
77
|
+
return docs[:limit]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Generator, Any
|
|
1
|
+
from typing import Dict, Generator, Any, AsyncGenerator
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
4
|
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
@@ -57,3 +57,21 @@ class ExecutorManager:
|
|
|
57
57
|
executor = self._executor_map[executor_type]
|
|
58
58
|
yield from executor.execute_stream(workflow_id, version, inputs, context)
|
|
59
59
|
|
|
60
|
+
async def exec_workflow_async(
|
|
61
|
+
self,
|
|
62
|
+
workflow_id: str,
|
|
63
|
+
version: str,
|
|
64
|
+
inputs: dict,
|
|
65
|
+
context: dict,
|
|
66
|
+
executor_type: ExecutorType | str = ExecutorType.NAIVE
|
|
67
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
68
|
+
if isinstance(executor_type, str):
|
|
69
|
+
executor_type = ExecutorType(executor_type)
|
|
70
|
+
if executor_type not in self._executor_map:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Unsupported executor kind: {executor_type}. "
|
|
73
|
+
f"Supported kinds: {list(self._executor_map.keys())}"
|
|
74
|
+
)
|
|
75
|
+
executor = self._executor_map[executor_type]
|
|
76
|
+
async for event in executor.execute_stream_async(workflow_id, version, inputs, context):
|
|
77
|
+
yield event
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Generator, Dict
|
|
2
|
+
from typing import Generator, Dict, AsyncGenerator
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class BaseExecutor(ABC):
|
|
@@ -7,3 +7,7 @@ class BaseExecutor(ABC):
|
|
|
7
7
|
def execute_stream(self, workflow_id: str, version: str, inputs: dict, context: dict) -> Generator[Dict, None, None]:
|
|
8
8
|
pass
|
|
9
9
|
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def execute_stream_async(self, workflow_id: str, version: str, inputs: dict, context: dict) -> AsyncGenerator[Dict, None]:
|
|
12
|
+
pass
|
|
13
|
+
|
|
@@ -2,15 +2,18 @@ import asyncio
|
|
|
2
2
|
import traceback
|
|
3
3
|
import queue
|
|
4
4
|
import threading
|
|
5
|
-
from typing import Generator, Dict,
|
|
5
|
+
from typing import Generator, Dict, Any, AsyncGenerator
|
|
6
6
|
|
|
7
7
|
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
8
|
+
from alayaflow.utils.coroutine import iter_sync
|
|
8
9
|
from alayaflow.workflow.workflow_manager import WorkflowManager
|
|
9
10
|
from alayaflow.workflow.runnable import BaseRunnableWorkflow, StateGraphRunnableWorkflow
|
|
10
11
|
from alayaflow.common.config import settings
|
|
11
12
|
from alayaflow.execution.langfuse_tracing import get_tracing
|
|
13
|
+
from alayaflow.utils.logger import AlayaFlowLogger
|
|
14
|
+
|
|
15
|
+
logger = AlayaFlowLogger()
|
|
12
16
|
|
|
13
|
-
_SENTINEL = object()
|
|
14
17
|
|
|
15
18
|
class NaiveExecutor(BaseExecutor):
|
|
16
19
|
def __init__(self, workflow_manager: WorkflowManager):
|
|
@@ -39,65 +42,30 @@ class NaiveExecutor(BaseExecutor):
|
|
|
39
42
|
inputs: dict,
|
|
40
43
|
context: dict
|
|
41
44
|
) -> Generator[Dict, None, None]:
|
|
45
|
+
return iter_sync(self.execute_stream_async(workflow_id, version, inputs, context))
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
async def execute_stream_async(
|
|
48
|
+
self,
|
|
49
|
+
workflow_id: str,
|
|
50
|
+
version: str,
|
|
51
|
+
inputs: dict,
|
|
52
|
+
context: dict
|
|
53
|
+
) -> AsyncGenerator[Dict, None]:
|
|
44
54
|
try:
|
|
45
55
|
runnable = self.workflow_manager.get_workflow_runnable(workflow_id, version)
|
|
46
56
|
except ValueError as e:
|
|
47
57
|
yield {"error": str(e), "workflow_id": workflow_id, "version": version}
|
|
48
58
|
return
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
logger.info(f"Execute workflow.\n{workflow_id=} {version=}\n{inputs=}\n{context=}")
|
|
51
61
|
|
|
52
62
|
# TODO: Support langflow workflow
|
|
53
|
-
# Only support StateGraphRunnableWorkflow now
|
|
54
|
-
#
|
|
63
|
+
# Only support StateGraphRunnableWorkflow for now
|
|
64
|
+
# check it as we pass a langgraph specific config here
|
|
55
65
|
if not isinstance(runnable, StateGraphRunnableWorkflow):
|
|
56
66
|
raise ValueError(f"NaiveExecutor only supports StateGraphRunnableWorkflow, but got {type(runnable)}")
|
|
57
67
|
|
|
58
|
-
# 3) async -> sync bridge
|
|
59
|
-
event_queue = queue.Queue(maxsize=1000)
|
|
60
|
-
|
|
61
|
-
def run_async_producer():
|
|
62
|
-
try:
|
|
63
|
-
asyncio.run(self._produce_events_to_queue(runnable, inputs, context, event_queue))
|
|
64
|
-
except Exception as e:
|
|
65
|
-
event_queue.put({"error": str(e), "traceback": traceback.format_exc(), "workflow_id": workflow_id, "version": version})
|
|
66
|
-
finally:
|
|
67
|
-
event_queue.put(_SENTINEL)
|
|
68
|
-
|
|
69
|
-
producer_thread = threading.Thread(target=run_async_producer, daemon=True)
|
|
70
|
-
producer_thread.start()
|
|
71
|
-
|
|
72
|
-
# 4) stream
|
|
73
|
-
while True:
|
|
74
|
-
try:
|
|
75
|
-
item = event_queue.get(timeout=1.0)
|
|
76
|
-
except queue.Empty:
|
|
77
|
-
if not producer_thread.is_alive():
|
|
78
|
-
saw_sentinel = False
|
|
79
|
-
while True:
|
|
80
|
-
try:
|
|
81
|
-
item = event_queue.get_nowait()
|
|
82
|
-
if item is _SENTINEL:
|
|
83
|
-
saw_sentinel = True
|
|
84
|
-
break
|
|
85
|
-
yield self._serialize_event(item)
|
|
86
|
-
except queue.Empty:
|
|
87
|
-
break
|
|
88
|
-
if saw_sentinel:
|
|
89
|
-
break
|
|
90
|
-
yield {"error": "producer thread exited unexpectedly", "workflow_id": workflow_id, "version": version}
|
|
91
|
-
break
|
|
92
|
-
continue
|
|
93
|
-
if item is _SENTINEL:
|
|
94
|
-
break
|
|
95
|
-
yield self._serialize_event(item)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
async def _produce_events_to_queue(self, runnable: BaseRunnableWorkflow, inputs: dict, context: dict, event_queue: queue.Queue):
|
|
99
68
|
try:
|
|
100
|
-
# Setup tracing
|
|
101
69
|
tracing = get_tracing(settings)
|
|
102
70
|
langfuse_cb = tracing.build_callback()
|
|
103
71
|
|
|
@@ -107,13 +75,7 @@ class NaiveExecutor(BaseExecutor):
|
|
|
107
75
|
config = {}
|
|
108
76
|
|
|
109
77
|
async for chunk in runnable.stream_events_async(inputs, context, config):
|
|
110
|
-
|
|
78
|
+
yield self._serialize_event(chunk)
|
|
111
79
|
except Exception as e:
|
|
112
|
-
|
|
113
|
-
event_queue.put({
|
|
114
|
-
"error": str(e),
|
|
115
|
-
"traceback": traceback.format_exc(),
|
|
116
|
-
"workflow_id": runnable.info.id,
|
|
117
|
-
"version": runnable.info.version
|
|
118
|
-
})
|
|
80
|
+
yield {"error": str(e), "traceback": traceback.format_exc(), "workflow_id": workflow_id, "version": version}
|
|
119
81
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import socket
|
|
2
2
|
import os
|
|
3
3
|
import struct
|
|
4
|
-
from typing import Generator, Dict, Optional
|
|
4
|
+
from typing import Generator, Dict, Optional, AsyncGenerator
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
@@ -123,3 +123,12 @@ class UvExecutor(BaseExecutor):
|
|
|
123
123
|
# if stderr:
|
|
124
124
|
# print(f"[Stderr] {stderr}")
|
|
125
125
|
|
|
126
|
+
|
|
127
|
+
def execute_stream_async(
|
|
128
|
+
self,
|
|
129
|
+
workflow_id: str,
|
|
130
|
+
version: str,
|
|
131
|
+
inputs: dict,
|
|
132
|
+
context: dict,
|
|
133
|
+
) -> AsyncGenerator[Dict, None]:
|
|
134
|
+
raise NotImplementedError("uv executor not supported yet")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Generator, Dict
|
|
1
|
+
from typing import Generator, Dict, AsyncGenerator
|
|
2
2
|
|
|
3
3
|
from alayaflow.execution.executors.base_executor import BaseExecutor
|
|
4
4
|
from alayaflow.workflow.workflow_manager import WorkflowManager
|
|
@@ -10,3 +10,5 @@ class WorkerExecutor(BaseExecutor):
|
|
|
10
10
|
def execute_stream(self, workflow_id: str, version: str, inputs: dict, context: dict) -> Generator[Dict, None, None]:
|
|
11
11
|
raise NotImplementedError("worker executor not supported yet")
|
|
12
12
|
|
|
13
|
+
def execute_stream_async(self, workflow_id: str, version: str, inputs: dict, context: dict) -> AsyncGenerator[Dict, None]:
|
|
14
|
+
raise NotImplementedError("worker executor not supported yet")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import queue, threading, asyncio
|
|
2
|
+
|
|
3
|
+
def iter_sync(async_gen):
|
|
4
|
+
"""将 AsyncGenerator 转为 Sync Generator"""
|
|
5
|
+
q = queue.Queue(maxsize=1)
|
|
6
|
+
SENTINEL = object()
|
|
7
|
+
|
|
8
|
+
def _run():
|
|
9
|
+
try:
|
|
10
|
+
async def _drive():
|
|
11
|
+
async for item in async_gen: q.put(item)
|
|
12
|
+
asyncio.run(_drive())
|
|
13
|
+
except Exception as e: q.put(e)
|
|
14
|
+
finally: q.put(SENTINEL)
|
|
15
|
+
|
|
16
|
+
threading.Thread(target=_run, daemon=True).start()
|
|
17
|
+
|
|
18
|
+
for item in iter(q.get, SENTINEL):
|
|
19
|
+
if isinstance(item, Exception): raise item
|
|
20
|
+
yield item
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from alayaflow.common.config import settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FILE_ONLY_KEY = "_file_only"
|
|
12
|
+
|
|
13
|
+
class AlayaFlowLogger:
|
|
14
|
+
_instance: Optional[logging.Logger] = None
|
|
15
|
+
_lock: threading.Lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def __new__(cls) -> logging.Logger:
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
with cls._lock:
|
|
20
|
+
# Double-check locking
|
|
21
|
+
if cls._instance is None:
|
|
22
|
+
cls._instance = logging.getLogger("alayaflow")
|
|
23
|
+
cls._initialize_logger(cls._instance)
|
|
24
|
+
return cls._instance
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _initialize_logger(cls, logger: logging.Logger):
|
|
28
|
+
if logger.hasHandlers():
|
|
29
|
+
logger.handlers.clear()
|
|
30
|
+
|
|
31
|
+
logger.setLevel(settings.log_level)
|
|
32
|
+
logger.propagate = False
|
|
33
|
+
|
|
34
|
+
formatter = logging.Formatter(
|
|
35
|
+
"[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d][pid:%(process)d] - %(message)s",
|
|
36
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
cls._add_console_handler(logger, formatter)
|
|
40
|
+
cls._add_safe_file_handler(logger, formatter)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _add_console_handler(logger: logging.Logger, formatter: logging.Formatter):
|
|
44
|
+
console = logging.StreamHandler(stream=sys.stderr)
|
|
45
|
+
console.setFormatter(formatter)
|
|
46
|
+
|
|
47
|
+
console.addFilter(lambda r: not getattr(r, FILE_ONLY_KEY, False))
|
|
48
|
+
|
|
49
|
+
if not settings.is_dev():
|
|
50
|
+
console.setLevel(logging.ERROR)
|
|
51
|
+
|
|
52
|
+
logger.addHandler(console)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _add_safe_file_handler(logger: logging.Logger, formatter: logging.Formatter):
|
|
56
|
+
"""尝试添加文件 Handler,如果失败(如权限问题)则仅记录一次警告"""
|
|
57
|
+
log_dir = Path(settings.log_dir)
|
|
58
|
+
log_path = log_dir / "alayaflow.log"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
file_handler = RotatingFileHandler(
|
|
64
|
+
log_path,
|
|
65
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
66
|
+
backupCount=5,
|
|
67
|
+
encoding='utf-8'
|
|
68
|
+
)
|
|
69
|
+
file_handler.setFormatter(formatter)
|
|
70
|
+
file_handler.setLevel(logging.DEBUG if settings.is_dev() else logging.INFO)
|
|
71
|
+
logger.addHandler(file_handler)
|
|
72
|
+
|
|
73
|
+
except (PermissionError, OSError) as e:
|
|
74
|
+
logger.warning(
|
|
75
|
+
"[AlayaFlowLogger] Cannot open log file under %s (%s); "
|
|
76
|
+
"logging will rely on console output.",
|
|
77
|
+
log_dir,
|
|
78
|
+
e,
|
|
79
|
+
)
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
alayaflow/__init__.py,sha256=
|
|
1
|
+
alayaflow/__init__.py,sha256=UbyJ0npjvUjHLpleqyLFoZxq8iy0qjgA8LwFKebXgeI,143
|
|
2
2
|
alayaflow/api/__init__.py,sha256=y6nWgqC3jhOffTqixKlv3OU_NEAFxbzHSvwJrE5bHNs,187
|
|
3
|
-
alayaflow/api/api_singleton.py,sha256=
|
|
4
|
-
alayaflow/clients/alayamem/base_client.py,sha256=
|
|
5
|
-
alayaflow/clients/alayamem/http_client.py,sha256=
|
|
6
|
-
alayaflow/common/config.py,sha256=
|
|
3
|
+
alayaflow/api/api_singleton.py,sha256=7gSytjx80q3Cs6Edfu4t5D3XyArrPTDerCaYjZ8EymA,12362
|
|
4
|
+
alayaflow/clients/alayamem/base_client.py,sha256=KYBHqnU2lfouQuLTSlNfIn8CyCk-IR9mRW_GzVqpxaI,989
|
|
5
|
+
alayaflow/clients/alayamem/http_client.py,sha256=oJHLYemhPa5KRwZAY-g9uzMM8haFLnTUBWkgX-G29Ks,2933
|
|
6
|
+
alayaflow/common/config.py,sha256=olyjg-csRt4dHwwHQ8Duh7qvAQXrkSBiBQ2OAoo6HuA,3874
|
|
7
7
|
alayaflow/component/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
alayaflow/component/chat_model.py,sha256=GH2xcOhtvh3YqN2LMITmu1j-0_-t9d4hiMuRFctF2og,402
|
|
9
9
|
alayaflow/component/intent_classifier.py,sha256=5KH52LIqIDpw2hlX4gi3Ff7SFVhenCFdFV-fXag0sDM,3765
|
|
10
|
-
alayaflow/component/llm_node.py,sha256=
|
|
11
|
-
alayaflow/component/memory.py,sha256=
|
|
12
|
-
alayaflow/component/retrieve_node.py,sha256=
|
|
10
|
+
alayaflow/component/llm_node.py,sha256=2QfiWew99EVQ05UpA7IkepefpOFHqb4Wxo_tYOipekc,3496
|
|
11
|
+
alayaflow/component/memory.py,sha256=ZWmpyJriSwNBP4nhQpq4q7bCnnLc6W_oVEJMSKegdLI,1738
|
|
12
|
+
alayaflow/component/retrieve_node.py,sha256=FlKyGH767ZbS4ss5SGcprp-sRsY4jA0YdlNP4OFzgQU,2842
|
|
13
13
|
alayaflow/component/search_node.py,sha256=JNFD6qXDd2_NPpXQf7z8xklJFZs7UrFhWet43nrs25Y,4950
|
|
14
14
|
alayaflow/component/web_search.py,sha256=HZp9j0X0YMBC_mhGqzi0g0pbvmlWiLiNdRxzbEFzl6s,3403
|
|
15
15
|
alayaflow/component/langflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -19,14 +19,16 @@ alayaflow/component/model/model_manager.py,sha256=VTqkCLXxhGqSXi7GcBxaRHw3wTTfEk
|
|
|
19
19
|
alayaflow/component/model/schemas.py,sha256=YADx6OGltGMzpUSNEYcUbGOrrJvtV6dFdAub3FfAw9w,1404
|
|
20
20
|
alayaflow/execution/__init__.py,sha256=9gj_xgIJxlq3EuwsDMb5X05yP70HoD0_T25Khd495Xc,117
|
|
21
21
|
alayaflow/execution/env_manager.py,sha256=-7npm1f-FNhHyOt8NZGbGA3i7JMHqQXoWV1uAe1bpyE,16744
|
|
22
|
-
alayaflow/execution/executor_manager.py,sha256=
|
|
22
|
+
alayaflow/execution/executor_manager.py,sha256=CQ-G7ROEge42-rJUXaRP_c_Us23-8epv2euKl_FbtpE,2944
|
|
23
23
|
alayaflow/execution/langfuse_tracing.py,sha256=BuRMHDH7Gub7CMkJM5ECLzs4vjy3VqAgzh2INE9zbOI,3882
|
|
24
24
|
alayaflow/execution/workflow_runner.py,sha256=XEX4Em0Hv1sI8Im0lREjXq3fN1jYVwFnMMW3pphIAZk,3243
|
|
25
25
|
alayaflow/execution/executors/__init__.py,sha256=RYwYg880smrZ8EX5iwVsJe0Rtgo8-tF82pY5jA3926g,412
|
|
26
|
-
alayaflow/execution/executors/base_executor.py,sha256=
|
|
27
|
-
alayaflow/execution/executors/naive_executor.py,sha256=
|
|
28
|
-
alayaflow/execution/executors/uv_executor.py,sha256=
|
|
29
|
-
alayaflow/execution/executors/worker_executor.py,sha256=
|
|
26
|
+
alayaflow/execution/executors/base_executor.py,sha256=bRgPlT5Z7Fwu1aje0Vvk5OdxBkLSmBw7cIEf2aVNP5Y,437
|
|
27
|
+
alayaflow/execution/executors/naive_executor.py,sha256=Jf2unKrOuon9R-4bQP5_n3GkjXwFPBBSZXBH7E2puBg,3149
|
|
28
|
+
alayaflow/execution/executors/uv_executor.py,sha256=VttozDIPPvoE2empkTs_qt9Spep5ZsC4bX84rUmokEs,4879
|
|
29
|
+
alayaflow/execution/executors/worker_executor.py,sha256=NN9PKTGZjxLBdLVcj-EFX9Mz46VPAA2VjCo1ole7wzU,726
|
|
30
|
+
alayaflow/utils/coroutine.py,sha256=709UGiY9dY-292YIVzTyqojg9HGigovfWK7R4-sPiyk,557
|
|
31
|
+
alayaflow/utils/logger.py,sha256=CIz8m14gQa3hgT7K4XbmNpplZPiEowS2MCbo19OLuMQ,2638
|
|
30
32
|
alayaflow/utils/singleton.py,sha256=5crFVfOkr9pU_j83ywqAMaL07BvVN5Ke_VGjT9qyUN0,432
|
|
31
33
|
alayaflow/workflow/__init__.py,sha256=mzqmL4P7q7ixTp2b0rZE-5iEBh7vv4YaTyvqnQZKmos,267
|
|
32
34
|
alayaflow/workflow/workflow_info.py,sha256=rnpAwYE4trhiv7o8LPmQpyQ3CDFfNN2yk1CLKRnWz0w,1259
|
|
@@ -35,7 +37,7 @@ alayaflow/workflow/workflow_manager.py,sha256=dUFS6B5V64mdsopxrM6f3LvJn497eTBqMJ
|
|
|
35
37
|
alayaflow/workflow/runnable/__init__.py,sha256=sNybFeRxLwbDLHiZxlVFXsn3w2n1Jn0Mtun2W6fvjFU,257
|
|
36
38
|
alayaflow/workflow/runnable/base_runnable_workflow.py,sha256=ap53fOeC5iUh2zm45LpEDjLJ4uqfO2C6FCN6WGm13kw,776
|
|
37
39
|
alayaflow/workflow/runnable/state_graph_runnable_workflow.py,sha256=PMSHks46kmNM2uDVmf5TNcLW7AR6dgfJFohxs8Dcfm4,972
|
|
38
|
-
alayaflow-0.1.
|
|
39
|
-
alayaflow-0.1.
|
|
40
|
-
alayaflow-0.1.
|
|
41
|
-
alayaflow-0.1.
|
|
40
|
+
alayaflow-0.1.4.dist-info/METADATA,sha256=bpHrxG1f9BLNqnUDYV7i1HULypER6DNFT57gPhHe1CM,1925
|
|
41
|
+
alayaflow-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
42
|
+
alayaflow-0.1.4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
43
|
+
alayaflow-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|