alayaflow 0.1.3__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 CHANGED
@@ -1,5 +1,7 @@
1
- """AlayaFlow - A desktop platform for executing LangGraph workflows with multiple executors."""
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.3"
5
7
 
@@ -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 init_session(self, user_id: str, session_id: str) -> Dict:
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 vdb_query(self, messages: List[Any], limit: int) -> Dict:
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 add_session_messages(self, user_id: str, session_id: str, messages: List[Any]) -> None:
24
+ def summary(self, session_id: str, user_id: str = None) -> Dict:
25
+ """ 查询会话摘要 - GET 请求 """
15
26
  pass
16
27
 
17
28
  @abstractmethod
18
- def query(self, user_id: str, session_id: str, message: Any) -> Dict:
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
- self.base_url = base_url.rstrip("/")
10
+ """Initialize the HTTP client with base URL"""
11
+ self.base_url = base_url.rstrip('/')
10
12
 
11
- def init_session(self, user_id, user_name, agent_name):
12
- return self._post("/init", {
13
- "user_id": user_id,
14
- "user_name": "unknown",
15
- "agent_name": "unknown",
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 vdb_query(self, messages: List[Any], limit: int, collection_name: str):
19
- query_text = self._extract_query(messages)
20
- return self._post("/vdb/query", {
21
- "collection_name": collection_name,
22
- "query_text": query_text,
23
- "limit": limit,
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 query(self, user_id, message):
27
- return self._post("/query", {
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
- # ---------- private helpers ----------
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__
@@ -1,6 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Literal, Optional
3
- from pydantic import model_validator
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
- agent_storage_path: Optional[Path] = None
28
- app_storage_path: Optional[Path] = None
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()
@@ -1,50 +1,52 @@
1
1
  from alayaflow.clients.alayamem.http_client import HttpAlayaMemClient
2
2
 
3
3
 
4
- def query_vdb_message(alayamem_url: str, messages, limit, collection_name = "file_watcher_collection"):
4
+ def query_file(alayamem_url: str, user_id: str, message):
5
5
  mem_client = HttpAlayaMemClient(alayamem_url)
6
6
  try:
7
- result = mem_client.vdb_query(messages, limit, collection_name)
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
- return {"vdb_results": [], "error": str(e)}
9
+ print(f"[Query File] Error: {e}")
10
+ return {"error": str(e)}
11
11
 
12
12
 
13
- def init_memory(alayamem_url: str, user_id, user_name="unknown", agent_name="unknown"):
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.init_session(user_id, user_name, agent_name)
18
+ return mem_client.commit_turn(session_id, user_text, assistant_text,
19
+ user_id, window_size)
17
20
  except Exception as e:
18
- return {"memory_initialized": False}
21
+ print(f"[Commit Turn] Error: {e}")
22
+ return {"error": str(e)}
19
23
 
20
24
 
21
- def query_message(alayamem_url: str, user_id, messages):
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
- last_message = messages[-1]
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
- resp = mem_client.query(user_id, last_message)
39
+ return mem_client.summary(session_id, user_id)
31
40
  except Exception as e:
32
- print(f"[Query User Message] Error: {e}")
33
- return []
41
+ print(f"[Summary] Error: {e}")
42
+ return {"error": str(e)}
34
43
 
35
- return resp
36
44
 
37
45
 
38
- def add_message(alayamem_url: str, user_id, session_id, messages):
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.add_session_messages(user_id, messages)
49
+ return mem_client.profile(user_id)
46
50
  except Exception as e:
47
- print(f"[Add Messages] Error: {e}")
48
- pass
49
-
50
- return
51
+ print(f"[Profile] Error: {e}")
52
+ return {"error": str(e)}
@@ -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, Optional, Any
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
- # 1) resolve workflow
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
- print(f"NaiveExecutor execute_stream: {workflow_id} {version} {inputs} {context}")
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
- # As in _produce_events_to_queue, we relay on the "configurable" (StateGraph only) to use langfuse tracing.
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
- event_queue.put(chunk) # Put each event immediately (real-time)
78
+ yield self._serialize_event(chunk)
111
79
  except Exception as e:
112
- # If execution fails, put error event in queue
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alayaflow
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A desktop platform for executing LangGraph workflows with uv-managed sandboxes.
5
5
  Author-email: alayaflow group <dev@example.com>
6
6
  License: MIT
@@ -1,14 +1,14 @@
1
- alayaflow/__init__.py,sha256=ZQGg3KVDYQcq_LK1TLcFuRtc1KFKYva_AoumPeVlOMM,121
1
+ alayaflow/__init__.py,sha256=UbyJ0npjvUjHLpleqyLFoZxq8iy0qjgA8LwFKebXgeI,143
2
2
  alayaflow/api/__init__.py,sha256=y6nWgqC3jhOffTqixKlv3OU_NEAFxbzHSvwJrE5bHNs,187
3
- alayaflow/api/api_singleton.py,sha256=SvWeLHPYHT1ABACwmoNj2dWrFjpqLCpSMz1-KW3hsZQ,11640
4
- alayaflow/clients/alayamem/base_client.py,sha256=pyU2WF2jqNEgBEe8JOZSg13gHQ2pJcBgJ_6YP-5mWkw,540
5
- alayaflow/clients/alayamem/http_client.py,sha256=n0hAh_ddzEwFfNMsdw5s2dqvuMsyZNGOT-RcHsIXuEw,2171
6
- alayaflow/common/config.py,sha256=pi4zH_Pi0u6Fb8ZIs4u3qFOUOUeqxxUqqksoUp-hynM,3806
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
10
  alayaflow/component/llm_node.py,sha256=2QfiWew99EVQ05UpA7IkepefpOFHqb4Wxo_tYOipekc,3496
11
- alayaflow/component/memory.py,sha256=Xl5ABW89dswC9oMwkeS6NFAZniKFw8BuGuLAAHddRRA,1448
11
+ alayaflow/component/memory.py,sha256=ZWmpyJriSwNBP4nhQpq4q7bCnnLc6W_oVEJMSKegdLI,1738
12
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
@@ -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=_t_tr58yUsMbIW_xfevUXv8ba-7lGbBJFhjPsQ-ricM,2147
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=mtBJM9bo_VLWAG9nnuq9xCjlD83bsvk1NPY5-aeD8TQ,254
27
- alayaflow/execution/executors/naive_executor.py,sha256=ICXslitv-9ONvJ3kLC-3-vTTBg1dWQlp0evCtxMO_MI,4749
28
- alayaflow/execution/executors/uv_executor.py,sha256=IIwP4j-BuDmfNoizuLsHcW0hRmzsLArWRVtVToX3_dM,4622
29
- alayaflow/execution/executors/worker_executor.py,sha256=niyTqsxB1iHvkuYb3xd35UwnsQllKul-Z6ikenJZ9Hk,513
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.3.dist-info/METADATA,sha256=aMfRyIwPukXW80Am5h8ApPNiBbL4mRZcfaOtMQflVL4,1925
39
- alayaflow-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
40
- alayaflow-0.1.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
41
- alayaflow-0.1.3.dist-info/RECORD,,
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,,