process-gpt-agent-sdk 0.2.10__py3-none-any.whl → 0.3.10__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.3.10.dist-info/METADATA +336 -0
- process_gpt_agent_sdk-0.3.10.dist-info/RECORD +5 -0
- processgpt_agent_sdk/processgpt_agent_framework.py +402 -0
- process_gpt_agent_sdk-0.2.10.dist-info/METADATA +0 -1026
- process_gpt_agent_sdk-0.2.10.dist-info/RECORD +0 -19
- processgpt_agent_sdk/__init__.py +0 -11
- processgpt_agent_sdk/core/__init__.py +0 -0
- processgpt_agent_sdk/core/database.py +0 -464
- processgpt_agent_sdk/server.py +0 -313
- processgpt_agent_sdk/simulator.py +0 -231
- processgpt_agent_sdk/tools/__init__.py +0 -0
- processgpt_agent_sdk/tools/human_query_tool.py +0 -211
- processgpt_agent_sdk/tools/knowledge_tools.py +0 -206
- processgpt_agent_sdk/tools/safe_tool_loader.py +0 -209
- processgpt_agent_sdk/utils/__init__.py +0 -0
- processgpt_agent_sdk/utils/context_manager.py +0 -45
- processgpt_agent_sdk/utils/crewai_event_listener.py +0 -205
- processgpt_agent_sdk/utils/event_handler.py +0 -72
- processgpt_agent_sdk/utils/logger.py +0 -97
- processgpt_agent_sdk/utils/summarizer.py +0 -146
- {process_gpt_agent_sdk-0.2.10.dist-info → process_gpt_agent_sdk-0.3.10.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.2.10.dist-info → process_gpt_agent_sdk-0.3.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import uuid
|
|
8
|
+
import os
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
12
|
+
from a2a.server.events import EventQueue, Event
|
|
13
|
+
from a2a.types import (
|
|
14
|
+
TaskArtifactUpdateEvent,
|
|
15
|
+
TaskState,
|
|
16
|
+
TaskStatusUpdateEvent,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# DB 어댑터 사용
|
|
20
|
+
from database import (
|
|
21
|
+
initialize_db,
|
|
22
|
+
polling_pending_todos,
|
|
23
|
+
record_event,
|
|
24
|
+
save_task_result,
|
|
25
|
+
update_task_error,
|
|
26
|
+
get_consumer_id,
|
|
27
|
+
fetch_agent_data,
|
|
28
|
+
fetch_all_agents,
|
|
29
|
+
fetch_form_types,
|
|
30
|
+
fetch_tenant_mcp_config,
|
|
31
|
+
fetch_human_users_by_proc_inst_id,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
load_dotenv()
|
|
35
|
+
|
|
36
|
+
logging.basicConfig(level=logging.INFO)
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TodoListRowContext:
|
|
41
|
+
"""fetch_pending_task/… 로부터 받은 todolist 행을 감싼 컨텍스트용 DTO"""
|
|
42
|
+
row: Dict[str, Any]
|
|
43
|
+
|
|
44
|
+
class ProcessGPTRequestContext(RequestContext):
|
|
45
|
+
"""DB row(스키마 준수) 기반 RequestContext 구현"""
|
|
46
|
+
def __init__(self, row: Dict[str, Any]):
|
|
47
|
+
self.row = row
|
|
48
|
+
self._user_input = (row.get('description') or '').strip()
|
|
49
|
+
self._message = self._user_input
|
|
50
|
+
self._current_task = None
|
|
51
|
+
self._task_state = row.get('draft_status') or ''
|
|
52
|
+
self._extra_context: Dict[str, Any] = {}
|
|
53
|
+
|
|
54
|
+
async def prepare_context(self) -> None:
|
|
55
|
+
"""database.py를 활용해 부가 컨텍스트를 미리 준비한다. 실패해도 기본값으로 계속 진행."""
|
|
56
|
+
logger.info("\n🔧 컨텍스트 준비 중...")
|
|
57
|
+
logger.info(" 📋 Task: %s", self.row.get('id'))
|
|
58
|
+
logger.info(" 🛠️ Tool: %s", self.row.get('tool') or 'N/A')
|
|
59
|
+
logger.info(" 🏢 Tenant: %s", self.row.get('tenant_id') or 'N/A')
|
|
60
|
+
effective_proc_inst_id = self.row.get('root_proc_inst_id') or self.row.get('proc_inst_id')
|
|
61
|
+
tool_val = self.row.get('tool') or ''
|
|
62
|
+
tenant_id = self.row.get('tenant_id') or ''
|
|
63
|
+
user_ids = self.row.get('user_id') or ''
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
notif_task = fetch_human_users_by_proc_inst_id(effective_proc_inst_id)
|
|
67
|
+
mcp_task = fetch_tenant_mcp_config(tenant_id)
|
|
68
|
+
form_task = fetch_form_types(tool_val, tenant_id)
|
|
69
|
+
agents_task = fetch_agent_data(user_ids or '')
|
|
70
|
+
|
|
71
|
+
notify_emails, tenant_mcp, form_tuple, agents = await asyncio.gather(
|
|
72
|
+
notif_task, mcp_task, form_task, agents_task
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not agents:
|
|
76
|
+
agents = await fetch_all_agents()
|
|
77
|
+
|
|
78
|
+
form_id, form_fields, form_html = form_tuple
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.exception(
|
|
81
|
+
"prepare_context failed (proc_inst_id=%s, todolist_id=%s): %s",
|
|
82
|
+
effective_proc_inst_id,
|
|
83
|
+
self.row.get('id'),
|
|
84
|
+
str(e),
|
|
85
|
+
)
|
|
86
|
+
notify_emails, tenant_mcp = "", None
|
|
87
|
+
form_id, form_fields, form_html = None, [], None
|
|
88
|
+
agents = []
|
|
89
|
+
|
|
90
|
+
self._extra_context = {
|
|
91
|
+
'id': self.row.get('id'),
|
|
92
|
+
'proc_inst_id': effective_proc_inst_id,
|
|
93
|
+
'root_proc_inst_id': self.row.get('root_proc_inst_id'),
|
|
94
|
+
'activity_name': self.row.get('activity_name'),
|
|
95
|
+
'agents': agents,
|
|
96
|
+
'tenant_mcp': tenant_mcp,
|
|
97
|
+
'form_fields': form_fields,
|
|
98
|
+
'form_html': form_html,
|
|
99
|
+
'form_id': form_id,
|
|
100
|
+
'notify_user_emails': notify_emails,
|
|
101
|
+
}
|
|
102
|
+
logger.info("\n✅ 컨텍스트 준비 완료!")
|
|
103
|
+
logger.info(" 📋 Task: %s", self.row.get('id'))
|
|
104
|
+
logger.info(" 🤖 Agents: %d개", len(agents) if isinstance(agents, list) else 0)
|
|
105
|
+
logger.info(" ⚡ Activity: %s", self.row.get('activity_name'))
|
|
106
|
+
logger.info(" 🏭 Process: %s", self.row.get('proc_inst_id'))
|
|
107
|
+
logger.info(" 🏢 Tenant: %s", self.row.get('tenant_id'))
|
|
108
|
+
logger.info("")
|
|
109
|
+
|
|
110
|
+
def get_user_input(self) -> str:
|
|
111
|
+
return self._user_input
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def message(self) -> str:
|
|
115
|
+
return self._message
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def current_task(self):
|
|
119
|
+
return self._current_task
|
|
120
|
+
|
|
121
|
+
@current_task.setter
|
|
122
|
+
def current_task(self, task):
|
|
123
|
+
self._current_task = task
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def task_state(self) -> str:
|
|
127
|
+
return self._task_state
|
|
128
|
+
|
|
129
|
+
def get_context_data(self) -> Dict[str, Any]:
|
|
130
|
+
return {
|
|
131
|
+
'row': self.row,
|
|
132
|
+
'extras': self._extra_context,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ProcessGPTEventQueue(EventQueue):
|
|
136
|
+
"""Events 테이블에 이벤트를 저장하는 EventQueue 구현 (database.record_event 사용)"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, todolist_id: str, agent_orch: str, proc_inst_id: Optional[str]):
|
|
139
|
+
self.todolist_id = todolist_id
|
|
140
|
+
self.agent_orch = agent_orch
|
|
141
|
+
self.proc_inst_id = proc_inst_id
|
|
142
|
+
super().__init__()
|
|
143
|
+
|
|
144
|
+
def enqueue_event(self, event: Event):
|
|
145
|
+
"""A2A 이벤트 처리"""
|
|
146
|
+
try:
|
|
147
|
+
# 공식 식별자만 공통 추출 (metadata는 타입별로 필요 시만 읽음)
|
|
148
|
+
proc_inst_id_val = getattr(event, 'contextId', None) or self.proc_inst_id
|
|
149
|
+
todo_id_val = getattr(event, 'taskId', None) or str(self.todolist_id)
|
|
150
|
+
logger.info("\n📨 이벤트 수신: %s", type(event).__name__)
|
|
151
|
+
logger.info(" 📋 Task: %s", self.todolist_id)
|
|
152
|
+
logger.info(" 🔄 Process: %s", proc_inst_id_val)
|
|
153
|
+
|
|
154
|
+
# 1) 기본 매핑: Artifact → todolist 저장 (오직 결과물만). 실패해도 진행
|
|
155
|
+
if isinstance(event, TaskArtifactUpdateEvent):
|
|
156
|
+
try:
|
|
157
|
+
is_final = bool(
|
|
158
|
+
getattr(event, 'final', None)
|
|
159
|
+
or getattr(event, 'lastChunk', None)
|
|
160
|
+
or getattr(event, 'last_chunk', None)
|
|
161
|
+
or getattr(event, 'last', None)
|
|
162
|
+
)
|
|
163
|
+
artifact_content = self._extract_payload(event)
|
|
164
|
+
logger.info("\n💾 아티팩트 저장 중...")
|
|
165
|
+
logger.info(" 📋 Task: %s", self.todolist_id)
|
|
166
|
+
logger.info(" 🏁 Final: %s", "예" if is_final else "아니오")
|
|
167
|
+
asyncio.create_task(save_task_result(self.todolist_id, artifact_content, is_final))
|
|
168
|
+
logger.info(" ✅ 저장 완료!\n")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.exception(
|
|
171
|
+
"enqueue_event artifact save failed (todolist_id=%s, proc_inst_id=%s, event=%s): %s",
|
|
172
|
+
self.todolist_id,
|
|
173
|
+
self.proc_inst_id,
|
|
174
|
+
type(event).__name__,
|
|
175
|
+
str(e),
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# 2) 기본 매핑: Status → events 저장 (crew_type/event_type/status/job_id는 metadata). 실패해도 진행
|
|
180
|
+
if isinstance(event, TaskStatusUpdateEvent):
|
|
181
|
+
metadata = getattr(event, 'metadata', None)
|
|
182
|
+
if not isinstance(metadata, dict):
|
|
183
|
+
metadata = {}
|
|
184
|
+
crew_type_val = metadata.get('crew_type')
|
|
185
|
+
# 상태 기반 event_type 매핑 (input_required -> human_asked)
|
|
186
|
+
status_obj = getattr(event, 'status', None)
|
|
187
|
+
state_val = getattr(status_obj, 'state', None)
|
|
188
|
+
event_type_val = {TaskState.input_required: 'human_asked'}.get(state_val) or metadata.get('event_type')
|
|
189
|
+
status_val = metadata.get('status')
|
|
190
|
+
job_id_val = metadata.get('job_id')
|
|
191
|
+
try:
|
|
192
|
+
payload: Dict[str, Any] = {
|
|
193
|
+
'id': str(uuid.uuid4()),
|
|
194
|
+
'job_id': job_id_val,
|
|
195
|
+
'todo_id': str(todo_id_val),
|
|
196
|
+
'proc_inst_id': proc_inst_id_val,
|
|
197
|
+
'crew_type': crew_type_val,
|
|
198
|
+
'event_type': event_type_val,
|
|
199
|
+
'data': self._extract_payload(event),
|
|
200
|
+
'status': status_val or None,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logger.info("\n📝 상태 이벤트 기록 중...")
|
|
204
|
+
logger.info(" 📋 Task: %s", self.todolist_id)
|
|
205
|
+
logger.info(" 🆔 Job: %s", job_id_val or 'N/A')
|
|
206
|
+
logger.info(" 🏷️ Type: %s", crew_type_val or 'N/A')
|
|
207
|
+
logger.info(" 🔍 Event: %s", event_type_val or 'N/A')
|
|
208
|
+
asyncio.create_task(record_event(payload))
|
|
209
|
+
logger.info(" ✅ 기록 완료!\n")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.exception(
|
|
212
|
+
"enqueue_event status record failed (todolist_id=%s, proc_inst_id=%s, job_id=%s, crew_type=%s): %s",
|
|
213
|
+
self.todolist_id,
|
|
214
|
+
self.proc_inst_id,
|
|
215
|
+
job_id_val,
|
|
216
|
+
crew_type_val,
|
|
217
|
+
str(e),
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to enqueue event: {e}")
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
def _extract_payload(self, event: Event) -> Any:
|
|
226
|
+
"""이벤트에서 실질 페이로드를 추출한다."""
|
|
227
|
+
try:
|
|
228
|
+
artifact_or_none = getattr(event, 'artifact', None)
|
|
229
|
+
status_or_none = getattr(event, 'status', None)
|
|
230
|
+
message_or_none = getattr(status_or_none, 'message', None)
|
|
231
|
+
|
|
232
|
+
source = artifact_or_none if artifact_or_none is not None else message_or_none
|
|
233
|
+
return self._parse_json_or_text(source)
|
|
234
|
+
except Exception:
|
|
235
|
+
return {}
|
|
236
|
+
|
|
237
|
+
def _parse_json_or_text(self, value: Any) -> Any:
|
|
238
|
+
"""간소화: new_* 유틸 출력(dict)과 문자열만 처리하여 순수 payload 반환."""
|
|
239
|
+
try:
|
|
240
|
+
# 1) None → 빈 구조
|
|
241
|
+
if value is None:
|
|
242
|
+
return {}
|
|
243
|
+
|
|
244
|
+
# 2) 문자열이면 JSON 파싱 시도
|
|
245
|
+
if isinstance(value, str):
|
|
246
|
+
text = value.strip()
|
|
247
|
+
if not text:
|
|
248
|
+
return ""
|
|
249
|
+
try:
|
|
250
|
+
return json.loads(text)
|
|
251
|
+
except Exception:
|
|
252
|
+
return text
|
|
253
|
+
|
|
254
|
+
# 3) 모델 → dict로 정규화 (있으면만)
|
|
255
|
+
try:
|
|
256
|
+
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
|
|
257
|
+
value = value.model_dump()
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
try:
|
|
261
|
+
if not isinstance(value, dict) and hasattr(value, "dict") and callable(getattr(value, "dict")):
|
|
262
|
+
value = value.dict()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
if not isinstance(value, dict) and hasattr(value, "__dict__"):
|
|
266
|
+
value = value.__dict__
|
|
267
|
+
|
|
268
|
+
# 4) dict만 대상으로 parts[0].text → parts[0].root.text → top-level text/content/data 순으로 추출
|
|
269
|
+
if isinstance(value, dict):
|
|
270
|
+
parts = value.get("parts")
|
|
271
|
+
if isinstance(parts, list) and parts:
|
|
272
|
+
first = parts[0] if isinstance(parts[0], dict) else None
|
|
273
|
+
if isinstance(first, dict):
|
|
274
|
+
text_candidate = (
|
|
275
|
+
first.get("text") or first.get("content") or first.get("data")
|
|
276
|
+
)
|
|
277
|
+
if not isinstance(text_candidate, str):
|
|
278
|
+
root = first.get("root") if isinstance(first.get("root"), dict) else None
|
|
279
|
+
if root:
|
|
280
|
+
text_candidate = (
|
|
281
|
+
root.get("text") or root.get("content") or root.get("data")
|
|
282
|
+
)
|
|
283
|
+
if isinstance(text_candidate, str):
|
|
284
|
+
return self._parse_json_or_text(text_candidate)
|
|
285
|
+
top_text = value.get("text") or value.get("content") or value.get("data")
|
|
286
|
+
if isinstance(top_text, str):
|
|
287
|
+
return self._parse_json_or_text(top_text)
|
|
288
|
+
return value
|
|
289
|
+
|
|
290
|
+
# 5) 그 외 타입은 원형 반환
|
|
291
|
+
return value
|
|
292
|
+
except Exception:
|
|
293
|
+
return {}
|
|
294
|
+
|
|
295
|
+
def task_done(self) -> None:
|
|
296
|
+
try:
|
|
297
|
+
payload: Dict[str, Any] = {
|
|
298
|
+
'id': str(uuid.uuid4()),
|
|
299
|
+
'job_id': "CREW_FINISHED",
|
|
300
|
+
'todo_id': str(self.todolist_id),
|
|
301
|
+
'proc_inst_id': self.proc_inst_id,
|
|
302
|
+
'crew_type': "crew",
|
|
303
|
+
'data': "Task completed successfully",
|
|
304
|
+
'event_type': 'crew_completed',
|
|
305
|
+
'status': None,
|
|
306
|
+
}
|
|
307
|
+
asyncio.create_task(record_event(payload))
|
|
308
|
+
logger.info("\n🏁 작업 완료 기록됨: %s\n", self.todolist_id)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to record task completion: {e}")
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
class ProcessGPTAgentServer:
|
|
314
|
+
"""DB 폴링 기반 Agent Server (database.py 사용)"""
|
|
315
|
+
|
|
316
|
+
def __init__(self, agent_executor: AgentExecutor, agent_type: str):
|
|
317
|
+
self.agent_executor = agent_executor
|
|
318
|
+
self.agent_orch = agent_type
|
|
319
|
+
self.polling_interval = 5 # seconds
|
|
320
|
+
self.is_running = False
|
|
321
|
+
|
|
322
|
+
async def run(self):
|
|
323
|
+
"""메인 실행 루프"""
|
|
324
|
+
self.is_running = True
|
|
325
|
+
logger.info("\n\n🚀 ===============================================")
|
|
326
|
+
logger.info(" ProcessGPT Agent Server STARTED")
|
|
327
|
+
logger.info(f" Agent Type: {self.agent_orch}")
|
|
328
|
+
logger.info("===============================================\n")
|
|
329
|
+
initialize_db()
|
|
330
|
+
|
|
331
|
+
while self.is_running:
|
|
332
|
+
try:
|
|
333
|
+
logger.info("\n🔍 Polling for tasks (agent_orch=%s)...", self.agent_orch)
|
|
334
|
+
row = await polling_pending_todos(self.agent_orch, get_consumer_id())
|
|
335
|
+
|
|
336
|
+
if row:
|
|
337
|
+
logger.info("\n\n✅ 새 작업 발견!")
|
|
338
|
+
logger.info("📋 Task ID: %s", row.get('id'))
|
|
339
|
+
logger.info("🔄 Process: %s", row.get('proc_inst_id'))
|
|
340
|
+
logger.info("⚡ Activity: %s", row.get('activity_name'))
|
|
341
|
+
logger.info("")
|
|
342
|
+
try:
|
|
343
|
+
await self.process_todolist_item(row)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.exception("process_todolist_item failed: %s", str(e))
|
|
346
|
+
try:
|
|
347
|
+
await self.mark_task_failed(str(row.get('id')), str(e))
|
|
348
|
+
except Exception as ee:
|
|
349
|
+
logger.exception("mark_task_failed failed: %s", str(ee))
|
|
350
|
+
await asyncio.sleep(self.polling_interval)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.exception(
|
|
354
|
+
"run loop error (agent_orch=%s): %s",
|
|
355
|
+
self.agent_orch,
|
|
356
|
+
str(e),
|
|
357
|
+
)
|
|
358
|
+
await asyncio.sleep(self.polling_interval)
|
|
359
|
+
|
|
360
|
+
async def process_todolist_item(self, row: Dict[str, Any]):
|
|
361
|
+
"""개별 todolist 항목을 처리"""
|
|
362
|
+
logger.info("\n\n🎯 ============== 작업 처리 시작 ==============")
|
|
363
|
+
logger.info("📝 Task ID: %s", row.get('id'))
|
|
364
|
+
logger.info("🔧 Tool: %s", row.get('tool'))
|
|
365
|
+
logger.info("🏭 Process: %s", row.get('proc_inst_id'))
|
|
366
|
+
logger.info("=" * 50 + "\n")
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
context = ProcessGPTRequestContext(row)
|
|
370
|
+
await context.prepare_context()
|
|
371
|
+
event_queue = ProcessGPTEventQueue(str(row.get('id')), self.agent_orch, row.get('proc_inst_id'))
|
|
372
|
+
await self.agent_executor.execute(context, event_queue)
|
|
373
|
+
event_queue.task_done()
|
|
374
|
+
logger.info("\n\n🎉 ============== 작업 완료 ==============")
|
|
375
|
+
logger.info("✨ Task ID: %s", row.get('id'))
|
|
376
|
+
logger.info("=" * 45 + "\n\n")
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.exception(
|
|
380
|
+
"process_todolist_item error (todolist_id=%s, proc_inst_id=%s): %s",
|
|
381
|
+
row.get('id'),
|
|
382
|
+
row.get('proc_inst_id'),
|
|
383
|
+
str(e),
|
|
384
|
+
)
|
|
385
|
+
await self.mark_task_failed(str(row.get('id')), str(e))
|
|
386
|
+
raise
|
|
387
|
+
|
|
388
|
+
async def mark_task_failed(self, todolist_id: str, error_message: str):
|
|
389
|
+
"""태스크 실패 처리 (DB 상태 업데이트)"""
|
|
390
|
+
try:
|
|
391
|
+
await update_task_error(todolist_id)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.exception(
|
|
394
|
+
"mark_task_failed error (todolist_id=%s): %s",
|
|
395
|
+
todolist_id,
|
|
396
|
+
str(e),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def stop(self):
|
|
400
|
+
"""서버 중지"""
|
|
401
|
+
self.is_running = False
|
|
402
|
+
logger.info("ProcessGPT Agent Server stopped")
|