deep-agent-cli 1.0.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.
app/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from app.core.LLM.factory import LLMFactory, LLMConfig
2
+
3
+ __all__ = ["LLMFactory", "LLMConfig"]
@@ -0,0 +1,10 @@
1
+ from langchain_anthropic import ChatAnthropic
2
+ from typing import Any, Dict, Optional
3
+
4
+ def create_anthropic_model(model_name: str, temperature: float, api_key: Optional[str], **extra_params: Any) -> ChatAnthropic:
5
+ return ChatAnthropic(
6
+ model=model_name,
7
+ temperature=temperature,
8
+ anthropic_api_key=api_key,
9
+ **extra_params
10
+ )
@@ -0,0 +1,74 @@
1
+ from typing import Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+ from langchain_core.language_models.chat_models import BaseChatModel
4
+
5
+ class LLMConfig(BaseModel):
6
+ """Cấu hình cho việc khởi tạo LLM."""
7
+ provider: str = Field(..., description="Nhà cung cấp (openai, anthropic, google, ollama, etc.)")
8
+ model_name: str = Field(..., description="Tên model cụ thể (gpt-4, claude-3, gemini-pro, etc.)")
9
+ temperature: float = Field(default=0, description="Độ sáng tạo của model (0-1)")
10
+ api_key: Optional[str] = Field(None, description="API Key nếu cần thiết")
11
+ base_url: Optional[str] = Field(None, description="Base URL cho các provider như Ollama hoặc Local LLMs")
12
+ extra_params: Dict[str, Any] = Field(default_factory=dict, description="Các tham số bổ sung khác")
13
+
14
+ class LLMFactory:
15
+ """
16
+ Factory class để quản lý việc khởi tạo các LLM từ nhiều nhà cung cấp khác nhau.
17
+ Logic khởi tạo cụ thể được tách ra các file provider riêng biệt.
18
+ """
19
+
20
+ @staticmethod
21
+ def create_llm(config: LLMConfig) -> BaseChatModel:
22
+ """
23
+ Khởi tạo và trả về một instance chuẩn của BaseChatModel.
24
+ Sử dụng Lazy Import để tránh lỗi khi thiếu thư viện của các provider không dùng tới.
25
+ """
26
+ provider = config.provider.lower()
27
+
28
+ try:
29
+ if provider == "openai":
30
+ from app.core.LLM.openai_provider import create_openai_model
31
+ return create_openai_model(
32
+ model_name=config.model_name,
33
+ temperature=config.temperature,
34
+ api_key=config.api_key,
35
+ **config.extra_params
36
+ )
37
+
38
+ elif provider == "anthropic":
39
+ from app.core.LLM.anthropic_provider import create_anthropic_model
40
+ return create_anthropic_model(
41
+ model_name=config.model_name,
42
+ temperature=config.temperature,
43
+ api_key=config.api_key,
44
+ **config.extra_params
45
+ )
46
+
47
+ elif provider == "google":
48
+ from app.core.LLM.google_provider import create_google_model
49
+ return create_google_model(
50
+ model_name=config.model_name,
51
+ temperature=config.temperature,
52
+ api_key=config.api_key,
53
+ **config.extra_params
54
+ )
55
+
56
+ elif provider == "ollama":
57
+ from app.core.LLM.ollama_provider import create_ollama_model
58
+ return create_ollama_model(
59
+ model_name=config.model_name,
60
+ temperature=config.temperature,
61
+ base_url=config.base_url,
62
+ **config.extra_params
63
+ )
64
+
65
+ else:
66
+ raise ValueError(f"Provider '{provider}' hiện chưa được hỗ trợ bởi Factory.")
67
+
68
+ except ImportError as e:
69
+ raise ImportError(
70
+ f"Lỗi: Thiếu thư viện cho provider '{provider}' hoặc lỗi cấu trúc file. "
71
+ f"Chi tiết: {str(e)}"
72
+ )
73
+ except Exception as e:
74
+ raise Exception(f"Lỗi hệ thống khi khởi tạo LLM ({provider}): {str(e)}")
@@ -0,0 +1,19 @@
1
+ import os
2
+ from typing import Optional, Any
3
+
4
+ from langchain_google_genai import ChatGoogleGenerativeAI
5
+
6
+
7
+ def create_google_model(model_name: str, temperature: float, api_key: Optional[str],
8
+ **extra_params: Any) -> ChatGoogleGenerativeAI:
9
+ """
10
+ Khởi tạo và trả về instance ChatGoogleGenerativeAI.
11
+ """
12
+ if os.getenv("GOOGLE_API_KEY") is None and api_key:
13
+ os.environ["GOOGLE_API_KEY"] = api_key
14
+
15
+ return ChatGoogleGenerativeAI(
16
+ model=model_name,
17
+ temperature=temperature,
18
+ **extra_params
19
+ )
@@ -0,0 +1,12 @@
1
+ from langchain_ollama.chat_models import ChatOllama
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ def create_ollama_model(model_name: str, temperature: float, base_url: Optional[str],
6
+ **extra_params: Any) -> ChatOllama:
7
+ return ChatOllama(
8
+ model=model_name,
9
+ temperature=temperature,
10
+ base_url=base_url,
11
+ **extra_params
12
+ )
@@ -0,0 +1,10 @@
1
+ from langchain_openai import ChatOpenAI
2
+ from typing import Any, Dict, Optional
3
+
4
+ def create_openai_model(model_name: str, temperature: float, api_key: Optional[str], **extra_params: Any) -> ChatOpenAI:
5
+ return ChatOpenAI(
6
+ model=model_name,
7
+ temperature=temperature,
8
+ api_key=api_key,
9
+ **extra_params
10
+ )
app/core/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from app.core.agent.deep_agent import DeepAgentFactory
2
+ from app.core.LLM.factory import LLMFactory, LLMConfig
3
+
4
+ __all__ = ["LLMFactory", "LLMConfig", "DeepAgentFactory"]
@@ -0,0 +1,3 @@
1
+ from app.core.agent.deep_agent import DeepAgentFactory, DeepAgentConfig
2
+
3
+ __all__ = ['DeepAgentFactory', 'DeepAgentConfig']
@@ -0,0 +1,109 @@
1
+ import os
2
+
3
+ import yaml
4
+ from typing import Any, Optional, Dict
5
+ from pydantic import BaseModel, Field
6
+ from deepagents import create_deep_agent, SubAgent
7
+ from deepagents.backends import FilesystemBackend
8
+
9
+ from app.core.LLM import LLMFactory, LLMConfig
10
+ from app.core.checkpoint import CheckpointFactory, CheckpointConfig
11
+
12
+
13
+ class DeepAgentConfig(BaseModel):
14
+ """Cấu hình toàn diện cho việc khởi tạo DeepAgent, hỗ trợ mở rộng các thuộc tính khác sau này."""
15
+ subagent_config_path: str = Field(default="config/subagent.yml",
16
+ description="Đường dẫn tới file config của subagent")
17
+ llm_config: LLMConfig = Field(..., description="Cấu hình LLM cho agent")
18
+ checkpoint_config: Optional[CheckpointConfig] = Field(default=None, description="Cấu hình checkpointer cho agent")
19
+ extra_params: Dict[str, Any] = Field(default_factory=dict,
20
+ description="Các thuộc tính cấu hình mở rộng khác trong tương lai")
21
+
22
+
23
+ # from core.tools import ReadFileTool, WriteFileTool, ShellExecuteTool
24
+
25
+
26
+ class DeepAgentFactory:
27
+ agent: dict = {}
28
+
29
+ config: dict = None
30
+
31
+ def __init__(self):
32
+ pass
33
+
34
+ @staticmethod
35
+ def load_subagents(config_path: str = "config/v1/subagent.yml") -> list[SubAgent]:
36
+ with open(config_path) as f:
37
+ config = yaml.safe_load(f)
38
+
39
+ sub_agents = []
40
+ for name, spec in config.items():
41
+ subagent = SubAgent(
42
+ name=name,
43
+ description=spec["description"],
44
+ system_prompt=spec["system_prompt"],
45
+ interrupt_on=spec["interrupt_on"],
46
+ )
47
+ if "model" in spec:
48
+ subagent["model"] = spec["model"]
49
+
50
+ sub_agents.append(subagent)
51
+ return sub_agents
52
+
53
+ @staticmethod
54
+ def get_config(config_path: str = None) -> Any:
55
+ if DeepAgentFactory.config is None and isinstance(config_path, str):
56
+ actual_path = config_path
57
+ if not os.path.exists(actual_path):
58
+ # Fallback: tìm kiếm dựa trên thư mục cài đặt của package
59
+ # deep_agent.py nằm ở <package_root>/app/core/agent/deep_agent.py
60
+ current_dir = os.path.dirname(os.path.abspath(__file__))
61
+ package_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
62
+ fallback_path = os.path.join(package_root, config_path.lstrip("/\\"))
63
+ if os.path.exists(fallback_path):
64
+ actual_path = fallback_path
65
+
66
+ with open(actual_path, encoding="utf-8") as f:
67
+ config = yaml.safe_load(f)
68
+ DeepAgentFactory.config = config
69
+ return DeepAgentFactory.config
70
+
71
+ @staticmethod
72
+ def create_agent(
73
+ config_or_path: Any,
74
+ llm_config: Optional[LLMConfig] = None,
75
+ checkpoint_config: Optional[CheckpointConfig] = None
76
+ ):
77
+ """
78
+ Khởi tạo DeepAgent từ cấu hình.
79
+ Hỗ trợ nhận một instance DeepAgentConfig hoặc các tham số riêng lẻ (tương thích ngược).
80
+ """
81
+ if isinstance(config_or_path, DeepAgentConfig):
82
+ agent_config = config_or_path
83
+ else:
84
+ # Hỗ trợ tương thích ngược với cách gọi cũ
85
+ agent_config = DeepAgentConfig(
86
+ subagent_config_path=config_or_path,
87
+ llm_config=llm_config,
88
+ checkpoint_config=checkpoint_config
89
+ )
90
+
91
+ config = DeepAgentFactory.get_config(os.path.join(os.getenv("CONFIG_PATH", "config/v0"), "config.yml"))
92
+ model = LLMFactory.create_llm(agent_config.llm_config)
93
+ sub_agent = DeepAgentFactory.load_subagents(agent_config.subagent_config_path)
94
+
95
+ backend = FilesystemBackend(virtual_mode=True)
96
+ sys_prompt = config["system_prompt"]
97
+
98
+ checkpoint_cfg = agent_config.checkpoint_config or CheckpointConfig(provider="inmemory")
99
+ checkpointer = CheckpointFactory.create_checkpoint(checkpoint_cfg)
100
+
101
+ return create_deep_agent(
102
+ model=model,
103
+ subagents=sub_agent,
104
+ # tools=tools,
105
+ system_prompt=sys_prompt,
106
+ # permissions=permissions,
107
+ backend=backend,
108
+ checkpointer=checkpointer,
109
+ )
@@ -0,0 +1,3 @@
1
+ from app.core.checkpoint.factory import CheckpointConfig, CheckpointFactory
2
+
3
+ __all__ = ["CheckpointConfig", "CheckpointFactory"]
@@ -0,0 +1,54 @@
1
+ from typing import Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+ from langgraph.checkpoint.base import BaseCheckpointSaver
4
+
5
+ class CheckpointConfig(BaseModel):
6
+ """Cấu hình cho việc khởi tạo Checkpointer."""
7
+ provider: str = Field(..., description="Nhà cung cấp checkpoint ('inmemory' hoặc 'sqlite')")
8
+ database_path: Optional[str] = Field(default=None, description="Đường dẫn đến file cơ sở dữ liệu SQLite (bắt buộc nếu provider là sqlite)")
9
+ extra_params: Dict[str, Any] = Field(default_factory=dict, description="Các tham số bổ sung khác")
10
+
11
+ class CheckpointFactory:
12
+ """
13
+ Factory class để quản lý việc khởi tạo các Checkpoint Savers từ nhiều nhà cung cấp khác nhau.
14
+ Chỉ cần hỗ trợ 'inmemory' và 'sqlite'.
15
+ """
16
+
17
+ @staticmethod
18
+ def create_checkpoint(config: CheckpointConfig) -> BaseCheckpointSaver:
19
+ """
20
+ Khởi tạo và trả về một checkpointer (BaseCheckpointSaver) tương ứng.
21
+ Sử dụng Lazy Import để tối ưu hiệu năng và tránh lỗi khi thiếu thư viện.
22
+ """
23
+ provider = config.provider.lower()
24
+
25
+ try:
26
+ if provider == "inmemory":
27
+ from langgraph.checkpoint.memory import MemorySaver
28
+ return MemorySaver(**config.extra_params)
29
+
30
+ elif provider == "sqlite":
31
+ if not config.database_path:
32
+ raise ValueError("SQLite checkpointer yêu cầu 'database_path' trong cấu hình.")
33
+
34
+ import sqlite3
35
+ from langgraph.checkpoint.sqlite import SqliteSaver
36
+
37
+ # check_same_thread=False là cần thiết cho SQLite trong môi trường đa luồng của Python
38
+ conn = sqlite3.connect(
39
+ config.database_path,
40
+ check_same_thread=False,
41
+ **config.extra_params
42
+ )
43
+ return SqliteSaver(conn)
44
+
45
+ else:
46
+ raise ValueError(f"Provider '{provider}' hiện chưa được hỗ trợ bởi CheckpointFactory.")
47
+
48
+ except ImportError as e:
49
+ raise ImportError(
50
+ f"Lỗi: Thiếu thư viện cho provider '{provider}' hoặc lỗi cấu trúc file. "
51
+ f"Chi tiết: {str(e)}"
52
+ )
53
+ except Exception as e:
54
+ raise Exception(f"Lỗi hệ thống khi khởi tạo Checkpointer ({provider}): {str(e)}")
@@ -0,0 +1,245 @@
1
+ import os
2
+ from typing import Any, Optional, Dict, List, Generator
3
+ from langgraph.types import Command
4
+ from app.service.ConfigService import ConfigService
5
+
6
+
7
+ class ChatService:
8
+ """
9
+ Service quản lý các cuộc hội thoại chat với agent của ConfigService.
10
+ Xử lý stream luồng suy nghĩ bằng phương thức agent.stream, ngắt human_in_the_loop và lưu lịch sử chat.
11
+ Áp dụng mẫu thiết kế Singleton để đảm bảo tính nhất quán của trạng thái phiên chat.
12
+ """
13
+ _instance = None
14
+
15
+ def __new__(cls, *args, **kwargs):
16
+ if not cls._instance:
17
+ cls._instance = super(ChatService, cls).__new__(cls, *args, **kwargs)
18
+ cls._instance._initialized = False
19
+ return cls._instance
20
+
21
+ def __init__(self):
22
+ if self._initialized:
23
+ return
24
+
25
+ # Lưu trữ trạng thái của các phiên hội thoại
26
+ # Cấu trúc: { thread_id: { "status": str, "pending_actions": list, "review_configs": list, "last_result": Any } }
27
+ self._sessions: Dict[str, Dict[str, Any]] = {}
28
+ self._initialized = True
29
+
30
+ def send_message(self, thread_id: str, message: str) -> Generator[Dict[str, Any], None, None]:
31
+ """
32
+ Gửi tin nhắn của người dùng đến Agent và stream luồng suy nghĩ.
33
+ Thực hiện kiểm tra lỗi đầu vào ngay lập tức và trả về một Generator sinh ra các chunk từ agent.stream.
34
+ Chunk cuối cùng được yield là kết quả tổng hợp của phiên.
35
+ """
36
+ agent = ConfigService().get_agent()
37
+ if agent is None:
38
+ raise ValueError("Không thể lấy Agent từ ConfigService")
39
+
40
+ def _stream_logic():
41
+ # Chuẩn bị dữ liệu đầu vào và cấu hình cho agent
42
+ input_data = {"messages": [{"role": "user", "content": message}]}
43
+ config = {"configurable": {"thread_id": thread_id}}
44
+
45
+ # Khởi tạo session nếu chưa tồn tại
46
+ if thread_id not in self._sessions:
47
+ self._sessions[thread_id] = {
48
+ "status": "completed",
49
+ "pending_actions": [],
50
+ "review_configs": [],
51
+ "last_result": None
52
+ }
53
+
54
+ # Thực thi agent.stream với cơ chế chống lỗi try-except
55
+ try:
56
+
57
+ for chunk in agent.stream(input_data, config=config, stream_mode="updates"):
58
+ yield chunk
59
+ except Exception as e:
60
+ print(f"[ChatService Error] Loi khi stream agent: {e}")
61
+ raise e
62
+
63
+ # Lấy trạng thái mới nhất từ agent sau khi kết thúc stream
64
+ try:
65
+ state = agent.get_state(config=config)
66
+ except Exception as e:
67
+ print(f"[ChatService Error] Loi khi lay state cuoi cung: {e}")
68
+ raise e
69
+
70
+ self._sessions[thread_id]["last_result"] = state
71
+
72
+ # Trích xuất interrupts từ state
73
+ interrupts = []
74
+ if state and hasattr(state, "tasks"):
75
+ for task in state.tasks:
76
+ if hasattr(task, "interrupts") and task.interrupts:
77
+ interrupts.extend(task.interrupts)
78
+
79
+ if interrupts:
80
+ interrupt = interrupts[0]
81
+ pending_actions = []
82
+
83
+ # Trích xuất pending actions từ interrupt value
84
+ if hasattr(interrupt, "value") and isinstance(interrupt.value, dict):
85
+ pending_actions = interrupt.value.get("action_requests", [])
86
+ elif isinstance(interrupt, dict):
87
+ pending_actions = interrupt.get("action_requests", [])
88
+
89
+ self._sessions[thread_id]["status"] = "waiting_approval"
90
+ self._sessions[thread_id]["pending_actions"] = pending_actions
91
+
92
+ final_res = {
93
+ "status": "waiting_approval",
94
+ "pending_actions": pending_actions
95
+ }
96
+ yield final_res
97
+ else:
98
+ # Trích xuất nội dung tin nhắn phản hồi cuối cùng từ agent
99
+ response_content = ""
100
+ messages = []
101
+ if state and hasattr(state, "values") and isinstance(state.values, dict):
102
+ messages = state.values.get("messages", [])
103
+
104
+ if messages:
105
+ last_msg = messages[-1]
106
+ if hasattr(last_msg, "content"):
107
+ response_content = last_msg.content
108
+ elif isinstance(last_msg, dict):
109
+ response_content = last_msg.get("content", "")
110
+
111
+ self._sessions[thread_id]["status"] = "completed"
112
+ self._sessions[thread_id]["pending_actions"] = []
113
+
114
+ final_res = {
115
+ "status": "completed",
116
+ "response": response_content
117
+ }
118
+ yield final_res
119
+
120
+ return _stream_logic()
121
+
122
+ def handle_approval(self, thread_id: str, decisions: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:
123
+ """
124
+ Xử lý quyết định phê duyệt (approve/reject) của người dùng đối với các tool call bị ngắt và stream phản hồi tiếp theo.
125
+ Thực hiện kiểm tra lỗi đầu vào ngay lập tức và trả về một Generator sinh ra các chunk từ agent.stream.
126
+ Chunk cuối cùng được yield là kết quả tổng hợp của phiên.
127
+ """
128
+ if thread_id not in self._sessions:
129
+ raise ValueError("Không tìm thấy phiên hội thoại")
130
+
131
+ session = self._sessions[thread_id]
132
+ if session.get("status") != "waiting_approval":
133
+ raise ValueError("Phiên hội thoại không ở trạng thái chờ duyệt")
134
+
135
+ agent = ConfigService().get_agent()
136
+ if agent is None:
137
+ raise ValueError("Không thể lấy Agent từ ConfigService")
138
+
139
+ def _stream_logic():
140
+ # Tạo command resume gửi cho agent
141
+ cmd = Command(resume={"decisions": decisions})
142
+ config = {"configurable": {"thread_id": thread_id}}
143
+
144
+ # Thực thi agent.stream tiếp tục với cơ chế chống lỗi try-except
145
+ try:
146
+ # stream_generator = agent.stream(cmd, config=config,stream_mode="values")
147
+ for chunk in agent.stream(cmd, config=config,stream_mode="updates"):
148
+ yield chunk
149
+ except Exception as e:
150
+ print(f"[ChatService Error] Loi khi stream tiep tuc agent: {e}")
151
+ raise e
152
+
153
+ # Lấy trạng thái mới nhất từ agent sau khi kết thúc stream
154
+ try:
155
+ state = agent.get_state(config=config)
156
+ except Exception as e:
157
+ print(f"[ChatService Error] Loi khi lay state cuoi cung: {e}")
158
+ raise e
159
+
160
+ self._sessions[thread_id]["last_result"] = state
161
+
162
+ # Trích xuất interrupts từ state
163
+ interrupts = []
164
+ if state and hasattr(state, "tasks"):
165
+ for task in state.tasks:
166
+ if hasattr(task, "interrupts") and task.interrupts:
167
+ interrupts.extend(task.interrupts)
168
+
169
+ if interrupts:
170
+ interrupt = interrupts[0]
171
+ pending_actions = []
172
+ if hasattr(interrupt, "value") and isinstance(interrupt.value, dict):
173
+ pending_actions = interrupt.value.get("action_requests", [])
174
+ elif isinstance(interrupt, dict):
175
+ pending_actions = interrupt.get("action_requests", [])
176
+
177
+ self._sessions[thread_id]["status"] = "waiting_approval"
178
+ self._sessions[thread_id]["pending_actions"] = pending_actions
179
+
180
+ final_res = {
181
+ "status": "waiting_approval",
182
+ "pending_actions": pending_actions
183
+ }
184
+ yield final_res
185
+ else:
186
+ response_content = ""
187
+ messages = []
188
+ if state and hasattr(state, "values") and isinstance(state.values, dict):
189
+ messages = state.values.get("messages", [])
190
+
191
+ if messages:
192
+ last_msg = messages[-1]
193
+ if hasattr(last_msg, "content"):
194
+ response_content = last_msg.content
195
+ elif isinstance(last_msg, dict):
196
+ response_content = last_msg.get("content", "")
197
+
198
+ self._sessions[thread_id]["status"] = "completed"
199
+ self._sessions[thread_id]["pending_actions"] = []
200
+
201
+ final_res = {
202
+ "status": "completed",
203
+ "response": response_content
204
+ }
205
+ yield final_res
206
+
207
+ return _stream_logic()
208
+
209
+ def get_history(self, thread_id: str) -> list:
210
+ """
211
+ Lấy toàn bộ lịch sử tin nhắn của phiên hội thoại (thread_id) từ agent state.
212
+ """
213
+ agent = ConfigService().get_agent()
214
+ if agent is None:
215
+ raise ValueError("Không thể lấy Agent từ ConfigService")
216
+
217
+ # Truy xuất state hiện tại của agent với cơ chế chống lỗi try-except
218
+ try:
219
+ state = agent.get_state(config={"configurable": {"thread_id": thread_id}})
220
+ except Exception as e:
221
+ print(f"[ChatService Error] Loi khi lay lich su tu agent: {e}")
222
+ return []
223
+
224
+ if not state or not hasattr(state, "values") or not state.values:
225
+ return []
226
+
227
+ return state.values.get("messages", [])
228
+
229
+ def get_session_status(self, thread_id: str) -> Optional[str]:
230
+ """
231
+ Lấy trạng thái hiện tại của phiên hội thoại (thread_id).
232
+ """
233
+ session = self._sessions.get(thread_id)
234
+ if session:
235
+ return session.get("status")
236
+ return None
237
+
238
+ def get_pending_actions(self, thread_id: str) -> List[Dict[str, Any]]:
239
+ """
240
+ Lấy danh sách các action đang chờ duyệt của phiên hội thoại (thread_id).
241
+ """
242
+ session = self._sessions.get(thread_id)
243
+ if session:
244
+ return session.get("pending_actions", [])
245
+ return []