process-gpt-agent-sdk 0.1.9__py3-none-any.whl → 0.2.1__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.
Potentially problematic release.
This version of process-gpt-agent-sdk might be problematic. Click here for more details.
- {process_gpt_agent_sdk-0.1.9.dist-info → process_gpt_agent_sdk-0.2.1.dist-info}/METADATA +1 -1
- process_gpt_agent_sdk-0.2.1.dist-info/RECORD +18 -0
- processgpt_agent_sdk/core/database.py +460 -508
- processgpt_agent_sdk/server.py +30 -5
- processgpt_agent_sdk/tools/human_query_tool.py +42 -34
- processgpt_agent_sdk/tools/safe_tool_loader.py +31 -7
- processgpt_agent_sdk/utils/context_manager.py +8 -0
- processgpt_agent_sdk/utils/crewai_event_listener.py +6 -2
- processgpt_agent_sdk/utils/summarizer.py +146 -122
- process_gpt_agent_sdk-0.1.9.dist-info/RECORD +0 -18
- {process_gpt_agent_sdk-0.1.9.dist-info → process_gpt_agent_sdk-0.2.1.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.1.9.dist-info → process_gpt_agent_sdk-0.2.1.dist-info}/top_level.txt +0 -0
processgpt_agent_sdk/server.py
CHANGED
|
@@ -23,6 +23,10 @@ from .utils.event_handler import process_event_message
|
|
|
23
23
|
from .utils.context_manager import set_context, reset_context
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# 서버: ProcessGPTAgentServer
|
|
28
|
+
# 설명: 작업 폴링→실행 준비→실행/이벤트 저장→취소 감시까지 담당하는 핵심 서버
|
|
29
|
+
# =============================================================================
|
|
26
30
|
class ProcessGPTAgentServer:
|
|
27
31
|
"""ProcessGPT 핵심 서버
|
|
28
32
|
|
|
@@ -31,6 +35,7 @@ class ProcessGPTAgentServer:
|
|
|
31
35
|
"""
|
|
32
36
|
|
|
33
37
|
def __init__(self, executor: AgentExecutor, polling_interval: int = 5, agent_orch: str = ""):
|
|
38
|
+
"""서버 실행기/폴링 주기/오케스트레이션 값을 초기화한다."""
|
|
34
39
|
self.polling_interval = polling_interval
|
|
35
40
|
self.is_running = False
|
|
36
41
|
self._executor: AgentExecutor = executor
|
|
@@ -39,6 +44,7 @@ class ProcessGPTAgentServer:
|
|
|
39
44
|
initialize_db()
|
|
40
45
|
|
|
41
46
|
async def run(self) -> None:
|
|
47
|
+
"""메인 폴링 루프를 실행한다. 작업을 가져와 준비/실행/감시를 순차 수행."""
|
|
42
48
|
self.is_running = True
|
|
43
49
|
write_log_message("ProcessGPT 서버 시작")
|
|
44
50
|
|
|
@@ -64,7 +70,6 @@ class ProcessGPTAgentServer:
|
|
|
64
70
|
await update_task_error(str(task_id))
|
|
65
71
|
except Exception as upd_err:
|
|
66
72
|
handle_application_error("FAILED 상태 업데이트 실패", upd_err, raise_error=False)
|
|
67
|
-
# 다음 루프로 진행
|
|
68
73
|
continue
|
|
69
74
|
|
|
70
75
|
except Exception as e:
|
|
@@ -72,10 +77,12 @@ class ProcessGPTAgentServer:
|
|
|
72
77
|
await asyncio.sleep(self.polling_interval)
|
|
73
78
|
|
|
74
79
|
def stop(self) -> None:
|
|
80
|
+
"""폴링 루프를 중지 플래그로 멈춘다."""
|
|
75
81
|
self.is_running = False
|
|
76
82
|
write_log_message("ProcessGPT 서버 중지")
|
|
77
83
|
|
|
78
84
|
async def _prepare_service_data(self, task_record: Dict[str, Any]) -> Dict[str, Any]:
|
|
85
|
+
"""실행에 필요한 데이터(에이전트/폼/요약/사용자)를 준비해 dict로 반환."""
|
|
79
86
|
done_outputs = await fetch_done_data(task_record.get("proc_inst_id"))
|
|
80
87
|
write_log_message(f"[PREP] done_outputs → {done_outputs}")
|
|
81
88
|
feedbacks = task_record.get("feedback")
|
|
@@ -86,7 +93,7 @@ class ProcessGPTAgentServer:
|
|
|
86
93
|
mcp_config = await fetch_tenant_mcp_config(str(task_record.get("tenant_id", "")))
|
|
87
94
|
write_log_message(f"[PREP] mcp_config(툴) → {mcp_config}")
|
|
88
95
|
|
|
89
|
-
form_id, form_types = await fetch_form_types(
|
|
96
|
+
form_id, form_types, form_html = await fetch_form_types(
|
|
90
97
|
str(task_record.get("tool", "")),
|
|
91
98
|
str(task_record.get("tenant_id", ""))
|
|
92
99
|
)
|
|
@@ -108,6 +115,7 @@ class ProcessGPTAgentServer:
|
|
|
108
115
|
"mcp_config": mcp_config,
|
|
109
116
|
"form_id": form_id,
|
|
110
117
|
"form_types": form_types or [],
|
|
118
|
+
"form_html": form_html or "",
|
|
111
119
|
"activity_name": str(task_record.get("activity_name", "")),
|
|
112
120
|
"message": str(task_record.get("description", "")),
|
|
113
121
|
"agent_orch": str(task_record.get("agent_orch", "")),
|
|
@@ -120,12 +128,12 @@ class ProcessGPTAgentServer:
|
|
|
120
128
|
return prepared
|
|
121
129
|
|
|
122
130
|
async def _execute_with_cancel_watch(self, task_record: Dict[str, Any], prepared_data: Dict[str, Any]) -> None:
|
|
131
|
+
"""실행 태스크와 취소 감시 태스크를 동시에 운영한다."""
|
|
123
132
|
executor = self._executor
|
|
124
133
|
|
|
125
134
|
context = ProcessGPTRequestContext(prepared_data)
|
|
126
135
|
event_queue = ProcessGPTEventQueue(task_record)
|
|
127
136
|
|
|
128
|
-
# 실행 전 컨텍스트 변수 설정 (CrewAI 전역 리스너 등에서 활용)
|
|
129
137
|
try:
|
|
130
138
|
set_context(
|
|
131
139
|
todo_id=str(task_record.get("id")),
|
|
@@ -166,6 +174,7 @@ class ProcessGPTAgentServer:
|
|
|
166
174
|
write_log_message(f"[EXEC END] task_id={task_record.get('id')} agent={prepared_data.get('agent_orch','')}")
|
|
167
175
|
|
|
168
176
|
async def _watch_cancellation(self, task_record: Dict[str, Any], executor: AgentExecutor, context: RequestContext, event_queue: EventQueue, execute_task: asyncio.Task) -> None:
|
|
177
|
+
"""작업 상태를 주기적으로 확인해 취소 신호 시 안전 종료를 수행."""
|
|
169
178
|
todo_id = str(task_record.get("id"))
|
|
170
179
|
|
|
171
180
|
while True:
|
|
@@ -191,34 +200,47 @@ class ProcessGPTAgentServer:
|
|
|
191
200
|
handle_application_error("취소 후 이벤트 큐 종료 실패", e, raise_error=False)
|
|
192
201
|
break
|
|
193
202
|
|
|
194
|
-
|
|
203
|
+
# =============================================================================
|
|
204
|
+
# 요청 컨텍스트: ProcessGPTRequestContext
|
|
205
|
+
# 설명: 실행기에게 전달되는 요청 데이터/상태를 캡슐화
|
|
206
|
+
# =============================================================================
|
|
195
207
|
class ProcessGPTRequestContext(RequestContext):
|
|
196
208
|
def __init__(self, prepared_data: Dict[str, Any]):
|
|
209
|
+
"""실행에 필요한 데이터 묶음을 보관한다."""
|
|
197
210
|
self._prepared_data = prepared_data
|
|
198
211
|
self._message = prepared_data.get("message", "")
|
|
199
212
|
self._current_task = None
|
|
200
213
|
|
|
201
214
|
def get_user_input(self) -> str:
|
|
215
|
+
"""사용자 입력 메시지를 반환한다."""
|
|
202
216
|
return self._message
|
|
203
217
|
|
|
204
218
|
@property
|
|
205
219
|
def message(self) -> str:
|
|
220
|
+
"""현재 메시지(사용자 입력)를 반환한다."""
|
|
206
221
|
return self._message
|
|
207
222
|
|
|
208
223
|
@property
|
|
209
224
|
def current_task(self):
|
|
225
|
+
"""현재 실행 중 태스크(있다면)를 반환한다."""
|
|
210
226
|
return getattr(self, "_current_task", None)
|
|
211
227
|
|
|
212
228
|
def get_context_data(self) -> Dict[str, Any]:
|
|
229
|
+
"""실행 컨텍스트 전체 데이터를 dict로 반환한다."""
|
|
213
230
|
return self._prepared_data
|
|
214
231
|
|
|
215
|
-
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# 이벤트 큐: ProcessGPTEventQueue
|
|
234
|
+
# 설명: 실행기 이벤트를 내부 큐에 넣고, 비동기 처리 태스크를 생성해 저장 로직 호출
|
|
235
|
+
# =============================================================================
|
|
216
236
|
class ProcessGPTEventQueue(EventQueue):
|
|
217
237
|
def __init__(self, task_record: Dict[str, Any]):
|
|
238
|
+
"""현재 처리 중인 작업 레코드를 보관한다."""
|
|
218
239
|
self.todo = task_record
|
|
219
240
|
super().__init__()
|
|
220
241
|
|
|
221
242
|
def enqueue_event(self, event: Event):
|
|
243
|
+
"""이벤트를 큐에 넣고, 백그라운드로 DB 저장 코루틴을 실행한다."""
|
|
222
244
|
try:
|
|
223
245
|
try:
|
|
224
246
|
super().enqueue_event(event)
|
|
@@ -230,15 +252,18 @@ class ProcessGPTEventQueue(EventQueue):
|
|
|
230
252
|
handle_application_error("이벤트 저장 실패", e, raise_error=False)
|
|
231
253
|
|
|
232
254
|
def task_done(self) -> None:
|
|
255
|
+
"""태스크 완료 로그를 남긴다."""
|
|
233
256
|
try:
|
|
234
257
|
write_log_message(f"태스크 완료: {self.todo['id']}")
|
|
235
258
|
except Exception as e:
|
|
236
259
|
handle_application_error("태스크 완료 처리 실패", e, raise_error=False)
|
|
237
260
|
|
|
238
261
|
async def close(self) -> None:
|
|
262
|
+
"""큐 종료 훅(필요 시 리소스 정리)."""
|
|
239
263
|
pass
|
|
240
264
|
|
|
241
265
|
def _create_bg_task(self, coro: Any, label: str) -> None:
|
|
266
|
+
"""백그라운드 태스크 생성 및 완료 콜백으로 예외 로깅."""
|
|
242
267
|
try:
|
|
243
268
|
task = asyncio.create_task(coro)
|
|
244
269
|
def _cb(t: asyncio.Task):
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Imports
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
3
7
|
import time
|
|
4
8
|
import uuid
|
|
5
|
-
import os
|
|
6
9
|
from typing import Optional, List, Literal, Type, Dict, Any
|
|
10
|
+
from datetime import datetime, timezone
|
|
7
11
|
|
|
8
12
|
from pydantic import BaseModel, Field
|
|
9
13
|
from crewai.tools import BaseTool
|
|
10
14
|
|
|
11
|
-
from ..utils.crewai_event_listener import CrewAIEventLogger
|
|
12
15
|
from ..utils.context_manager import todo_id_var, proc_id_var, all_users_var
|
|
13
16
|
from ..utils.logger import write_log_message, handle_application_error
|
|
14
|
-
from ..core.database import
|
|
17
|
+
from ..core.database import fetch_human_response_sync, save_notification, initialize_db, get_db_client
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# 입력 스키마
|
|
22
|
+
# =============================================================================
|
|
17
23
|
class HumanQuerySchema(BaseModel):
|
|
18
|
-
"""사용자 확인/추가정보
|
|
24
|
+
"""사용자 확인/추가정보 요청 입력 스키마."""
|
|
19
25
|
|
|
20
26
|
role: str = Field(..., description="누구에게(역할 또는 대상)")
|
|
21
27
|
text: str = Field(..., description="질의 내용")
|
|
@@ -27,8 +33,12 @@ class HumanQuerySchema(BaseModel):
|
|
|
27
33
|
)
|
|
28
34
|
|
|
29
35
|
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# HumanQueryTool
|
|
38
|
+
# 설명: 보안/모호성 관련 질문을 사용자에게 전송하고 응답을 동기 대기
|
|
39
|
+
# =============================================================================
|
|
30
40
|
class HumanQueryTool(BaseTool):
|
|
31
|
-
"""사람에게
|
|
41
|
+
"""사람에게 질문을 보내고 응답을 동기적으로 대기한다."""
|
|
32
42
|
|
|
33
43
|
name: str = "human_asked"
|
|
34
44
|
description: str = (
|
|
@@ -95,6 +105,7 @@ class HumanQueryTool(BaseTool):
|
|
|
95
105
|
agent_name: Optional[str] = None,
|
|
96
106
|
**kwargs,
|
|
97
107
|
):
|
|
108
|
+
"""도구 실행 컨텍스트(테넌트/유저/프로세스)를 옵션으로 설정."""
|
|
98
109
|
super().__init__(**kwargs)
|
|
99
110
|
self._tenant_id = tenant_id
|
|
100
111
|
self._user_id = user_id
|
|
@@ -102,18 +113,19 @@ class HumanQueryTool(BaseTool):
|
|
|
102
113
|
self._proc_inst_id = proc_inst_id
|
|
103
114
|
self._agent_name = agent_name
|
|
104
115
|
|
|
105
|
-
#
|
|
116
|
+
# =============================================================================
|
|
117
|
+
# 동기 실행
|
|
118
|
+
# 설명: 질문 이벤트를 저장하고 DB 폴링으로 응답을 기다린다
|
|
119
|
+
# =============================================================================
|
|
106
120
|
def _run(
|
|
107
121
|
self, role: str, text: str, type: str = "text", options: Optional[List[str]] = None
|
|
108
122
|
) -> str:
|
|
123
|
+
"""질문 이벤트를 기록하고 최종 응답 문자열을 반환."""
|
|
109
124
|
try:
|
|
110
|
-
# 초기화된 기본 agent_name 사용
|
|
111
125
|
agent_name = getattr(self, "_agent_name", None)
|
|
112
|
-
|
|
113
126
|
write_log_message(f"HumanQueryTool 실행: role={role}, agent_name={agent_name}, type={type}, options={options}")
|
|
114
127
|
query_id = f"human_asked_{uuid.uuid4()}"
|
|
115
128
|
|
|
116
|
-
# 이벤트 발행 데이터
|
|
117
129
|
payload: Dict[str, Any] = {
|
|
118
130
|
"role": role,
|
|
119
131
|
"text": text,
|
|
@@ -121,31 +133,31 @@ class HumanQueryTool(BaseTool):
|
|
|
121
133
|
"options": options or [],
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
# 컨텍스트 식별자
|
|
125
136
|
todo_id = todo_id_var.get() or self._todo_id
|
|
126
137
|
proc_inst_id = proc_id_var.get() or self._proc_inst_id
|
|
127
138
|
|
|
128
|
-
# 이벤트 발행
|
|
129
|
-
# 상태 정보는 data 안에 포함시켜 저장 (emit_event 시그니처에 status 없음)
|
|
130
139
|
payload_with_status = {
|
|
131
140
|
**payload,
|
|
132
141
|
"status": "ASKED",
|
|
133
142
|
"agent_profile": "/images/chat-icon.png"
|
|
134
143
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
todo_id
|
|
142
|
-
proc_inst_id
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
|
|
145
|
+
initialize_db()
|
|
146
|
+
supabase = get_db_client()
|
|
147
|
+
record = {
|
|
148
|
+
"id": str(uuid.uuid4()),
|
|
149
|
+
"job_id": query_id,
|
|
150
|
+
"todo_id": str(todo_id) if todo_id is not None else None,
|
|
151
|
+
"proc_inst_id": str(proc_inst_id) if proc_inst_id is not None else None,
|
|
152
|
+
"event_type": "human_asked",
|
|
153
|
+
"crew_type": "action",
|
|
154
|
+
"data": payload_with_status,
|
|
155
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
156
|
+
}
|
|
157
|
+
supabase.table("events").insert(record).execute()
|
|
158
|
+
|
|
146
159
|
try:
|
|
147
160
|
tenant_id = self._tenant_id
|
|
148
|
-
# 대상 이메일: context var(all_users_var)에 이메일 CSV가 있어야만 저장
|
|
149
161
|
target_emails_csv = all_users_var.get() or ""
|
|
150
162
|
if target_emails_csv and target_emails_csv.strip():
|
|
151
163
|
write_log_message(f"알림 저장 시도: target_emails_csv={target_emails_csv}, tenant_id={tenant_id}")
|
|
@@ -163,41 +175,37 @@ class HumanQueryTool(BaseTool):
|
|
|
163
175
|
except Exception as e:
|
|
164
176
|
handle_application_error("알림저장HumanTool", e, raise_error=False)
|
|
165
177
|
|
|
166
|
-
# 응답 폴링 (events 테이블에서 동일 job_id, event_type=human_response)
|
|
167
178
|
answer = self._wait_for_response(query_id)
|
|
168
179
|
return answer
|
|
169
180
|
except Exception as e:
|
|
170
|
-
# 사용자 미응답 또는 기타 에러 시에도 작업이 즉시 중단되지 않도록 문자열 반환
|
|
171
181
|
handle_application_error("HumanQueryTool", e, raise_error=False)
|
|
172
182
|
return "사용자 미응답 거절"
|
|
173
183
|
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# 응답 대기
|
|
186
|
+
# 설명: events 테이블에서 human_response를 폴링하여 응답을 가져온다
|
|
187
|
+
# =============================================================================
|
|
174
188
|
def _wait_for_response(
|
|
175
189
|
self, job_id: str, timeout_sec: int = 180, poll_interval_sec: int = 5
|
|
176
190
|
) -> str:
|
|
177
|
-
"""DB
|
|
191
|
+
"""DB 폴링으로 사람의 응답을 기다려 문자열로 반환."""
|
|
178
192
|
deadline = time.time() + timeout_sec
|
|
179
193
|
|
|
180
194
|
while time.time() < deadline:
|
|
181
195
|
try:
|
|
182
196
|
write_log_message(f"HumanQueryTool 응답 폴링: {job_id}")
|
|
183
|
-
event =
|
|
197
|
+
event = fetch_human_response_sync(job_id=job_id)
|
|
184
198
|
if event:
|
|
185
199
|
write_log_message(f"HumanQueryTool 응답 수신: {event}")
|
|
186
200
|
data = event.get("data") or {}
|
|
187
|
-
# 기대 형식: {"answer": str, ...}
|
|
188
201
|
answer = (data or {}).get("answer")
|
|
189
202
|
if isinstance(answer, str):
|
|
190
203
|
write_log_message("사람 응답 수신 완료")
|
|
191
204
|
return answer
|
|
192
|
-
# 문자열이 아니면 직렬화하여 반환
|
|
193
205
|
return str(data)
|
|
194
206
|
|
|
195
207
|
except Exception as e:
|
|
196
|
-
# 응답이 아직 없는 경우(0개 행) 또는 기타 DB 오류 시 계속 폴링
|
|
197
208
|
write_log_message(f"인간 응답 대기 중... (오류: {str(e)[:100]})")
|
|
198
|
-
|
|
199
209
|
time.sleep(poll_interval_sec)
|
|
200
|
-
|
|
201
|
-
# 타임아웃: 사용자 미응답으로 간주
|
|
202
210
|
return "사용자 미응답 거절"
|
|
203
211
|
|
|
@@ -9,23 +9,30 @@ from .knowledge_tools import Mem0Tool, MementoTool
|
|
|
9
9
|
from ..utils.logger import write_log_message, handle_application_error
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# SafeToolLoader
|
|
14
|
+
# 설명: 로컬/외부 MCP 도구들을 안전하게 초기화·로드·종료 관리
|
|
15
|
+
# =============================================================================
|
|
12
16
|
class SafeToolLoader:
|
|
13
17
|
"""도구 로더 클래스"""
|
|
14
|
-
adapters = []
|
|
18
|
+
adapters = []
|
|
15
19
|
|
|
16
20
|
ANYIO_PATCHED: bool = False
|
|
17
21
|
|
|
18
22
|
def __init__(self, tenant_id: Optional[str] = None, user_id: Optional[str] = None, agent_name: Optional[str] = None, mcp_config: Optional[Dict] = None):
|
|
23
|
+
"""실행 컨텍스트(tenant/user/agent)와 MCP 설정을 보관한다."""
|
|
19
24
|
self.tenant_id = tenant_id
|
|
20
25
|
self.user_id = user_id
|
|
21
26
|
self.agent_name = agent_name
|
|
22
|
-
# 외부에서 전달된 MCP 설정 사용 (DB 접근 금지)
|
|
23
27
|
self._mcp_servers = (mcp_config or {}).get('mcpServers', {})
|
|
24
28
|
self.local_tools = ["mem0", "memento", "human_asked"]
|
|
25
29
|
write_log_message(f"SafeToolLoader 초기화 완료 (tenant_id: {tenant_id}, user_id: {user_id})")
|
|
26
30
|
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Warmup (npx 서버 사전 준비)
|
|
33
|
+
# =============================================================================
|
|
27
34
|
def warmup_server(self, server_key: str, mcp_config: Optional[Dict] = None):
|
|
28
|
-
"""npx
|
|
35
|
+
"""npx 서버 패키지를 미리 캐싱해 최초 실행 지연을 줄인다."""
|
|
29
36
|
servers = (mcp_config or {}).get('mcpServers') or self._mcp_servers or {}
|
|
30
37
|
server_config = servers.get(server_key, {}) if isinstance(servers, dict) else {}
|
|
31
38
|
if not server_config or server_config.get("command") != "npx":
|
|
@@ -54,8 +61,11 @@ class SafeToolLoader:
|
|
|
54
61
|
except Exception:
|
|
55
62
|
pass
|
|
56
63
|
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# 유틸: npx 경로 탐색
|
|
66
|
+
# =============================================================================
|
|
57
67
|
def _find_npx_command(self) -> str:
|
|
58
|
-
"""npx
|
|
68
|
+
"""npx 실행 파일 경로를 탐색해 반환한다."""
|
|
59
69
|
try:
|
|
60
70
|
import shutil
|
|
61
71
|
npx_path = shutil.which("npx") or shutil.which("npx.cmd")
|
|
@@ -65,6 +75,9 @@ class SafeToolLoader:
|
|
|
65
75
|
pass
|
|
66
76
|
return "npx"
|
|
67
77
|
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# 로컬 도구 생성
|
|
80
|
+
# =============================================================================
|
|
68
81
|
def create_tools_from_names(self, tool_names: List[str], mcp_config: Optional[Dict] = None) -> List:
|
|
69
82
|
"""tool_names 리스트에서 실제 Tool 객체들 생성"""
|
|
70
83
|
if isinstance(tool_names, str):
|
|
@@ -88,6 +101,9 @@ class SafeToolLoader:
|
|
|
88
101
|
write_log_message(f"총 {len(tools)}개 도구 생성 완료")
|
|
89
102
|
return tools
|
|
90
103
|
|
|
104
|
+
# =============================================================================
|
|
105
|
+
# 로컬 도구 로더들
|
|
106
|
+
# =============================================================================
|
|
91
107
|
def _load_mem0(self) -> List:
|
|
92
108
|
"""mem0 도구 로드 - 에이전트별 메모리"""
|
|
93
109
|
try:
|
|
@@ -110,12 +126,14 @@ class SafeToolLoader:
|
|
|
110
126
|
def _load_human_asked(self) -> List:
|
|
111
127
|
"""human_asked 도구 로드 (선택사항: 사용 시 외부에서 주입)"""
|
|
112
128
|
try:
|
|
113
|
-
# 필요한 경우 외부에서 HumanQueryTool을 이 패키지에 추가하여 import하고 리턴하도록 변경 가능
|
|
114
129
|
return []
|
|
115
130
|
except Exception as error:
|
|
116
131
|
handle_application_error("툴human오류", error, raise_error=False)
|
|
117
132
|
return []
|
|
118
133
|
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# 외부 MCP 도구 로더
|
|
136
|
+
# =============================================================================
|
|
119
137
|
def _load_mcp_tool(self, tool_name: str, mcp_config: Optional[Dict] = None) -> List:
|
|
120
138
|
"""MCP 도구 로드 (timeout & retry 지원)"""
|
|
121
139
|
self._apply_anyio_patch()
|
|
@@ -157,8 +175,11 @@ class SafeToolLoader:
|
|
|
157
175
|
handle_application_error(f"툴{tool_name}오류", e, raise_error=False)
|
|
158
176
|
return []
|
|
159
177
|
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# anyio 서브프로세스 stderr 패치
|
|
180
|
+
# =============================================================================
|
|
160
181
|
def _apply_anyio_patch(self):
|
|
161
|
-
"""
|
|
182
|
+
"""stderr에 fileno 없음 대비: PIPE로 보정해 예외를 방지한다."""
|
|
162
183
|
if SafeToolLoader.ANYIO_PATCHED:
|
|
163
184
|
return
|
|
164
185
|
from anyio._core._subprocesses import open_process as _orig
|
|
@@ -173,9 +194,12 @@ class SafeToolLoader:
|
|
|
173
194
|
anyio._core._subprocesses.open_process = patched_open_process
|
|
174
195
|
SafeToolLoader.ANYIO_PATCHED = True
|
|
175
196
|
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# 종료 처리
|
|
199
|
+
# =============================================================================
|
|
176
200
|
@classmethod
|
|
177
201
|
def shutdown_all_adapters(cls):
|
|
178
|
-
"""모든 MCPServerAdapter
|
|
202
|
+
"""모든 MCPServerAdapter 연결을 안전하게 종료한다."""
|
|
179
203
|
for adapter in cls.adapters:
|
|
180
204
|
try:
|
|
181
205
|
adapter.stop()
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Context Manager
|
|
5
|
+
# 설명: 요청/프로세스 범위의 컨텍스트 값을 ContextVar로 관리
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
3
8
|
from contextvars import ContextVar
|
|
4
9
|
from typing import Optional
|
|
5
10
|
|
|
6
11
|
|
|
12
|
+
# 컨텍스트 변수 정의
|
|
7
13
|
todo_id_var: ContextVar[Optional[str]] = ContextVar("todo_id", default=None)
|
|
8
14
|
proc_id_var: ContextVar[Optional[str]] = ContextVar("proc_id", default=None)
|
|
9
15
|
crew_type_var: ContextVar[Optional[str]] = ContextVar("crew_type", default=None)
|
|
@@ -13,6 +19,7 @@ all_users_var: ContextVar[Optional[str]] = ContextVar("all_users", default=None)
|
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] = None, crew_type: Optional[str] = None, form_key: Optional[str] = None, form_id: Optional[str] = None, all_users: Optional[str] = None) -> None:
|
|
22
|
+
"""전달된 값들만 ContextVar에 설정한다."""
|
|
16
23
|
if todo_id is not None:
|
|
17
24
|
todo_id_var.set(todo_id)
|
|
18
25
|
if proc_inst_id is not None:
|
|
@@ -28,6 +35,7 @@ def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] =
|
|
|
28
35
|
|
|
29
36
|
|
|
30
37
|
def reset_context() -> None:
|
|
38
|
+
"""모든 컨텍스트 값을 초기 상태(None)로 되돌린다."""
|
|
31
39
|
todo_id_var.set(None)
|
|
32
40
|
proc_id_var.set(None)
|
|
33
41
|
crew_type_var.set(None)
|
|
@@ -21,6 +21,7 @@ class CrewAIEventLogger:
|
|
|
21
21
|
# Initialization
|
|
22
22
|
# =============================================================================
|
|
23
23
|
def __init__(self):
|
|
24
|
+
"""Supabase 클라이언트를 초기화한다."""
|
|
24
25
|
initialize_db()
|
|
25
26
|
self.supabase = get_db_client()
|
|
26
27
|
write_log_message("CrewAIEventLogger 초기화 완료")
|
|
@@ -151,7 +152,6 @@ class CrewAIEventLogger:
|
|
|
151
152
|
except Exception as e:
|
|
152
153
|
if attempt < 3:
|
|
153
154
|
handle_application_error("이벤트저장오류(재시도)", e, raise_error=False)
|
|
154
|
-
# 지수 백오프: 0.3s, 0.6s
|
|
155
155
|
import time
|
|
156
156
|
time.sleep(0.3 * attempt)
|
|
157
157
|
continue
|
|
@@ -178,7 +178,10 @@ class CrewAIEventLogger:
|
|
|
178
178
|
handle_application_error("이벤트처리오류", e, raise_error=False)
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
# =============================================================================
|
|
182
|
+
# CrewConfigManager
|
|
183
|
+
# 설명: 이벤트 리스너를 프로세스 단위로 1회 등록
|
|
184
|
+
# =============================================================================
|
|
182
185
|
class CrewConfigManager:
|
|
183
186
|
"""글로벌 CrewAI 이벤트 리스너 등록 매니저"""
|
|
184
187
|
_registered_by_pid: set[int] = set()
|
|
@@ -188,6 +191,7 @@ class CrewConfigManager:
|
|
|
188
191
|
self._register_once_per_process()
|
|
189
192
|
|
|
190
193
|
def _register_once_per_process(self) -> None:
|
|
194
|
+
"""현재 프로세스에만 한 번 이벤트 리스너를 등록한다."""
|
|
191
195
|
try:
|
|
192
196
|
pid = os.getpid()
|
|
193
197
|
if pid in self._registered_by_pid:
|