process-gpt-agent-sdk 0.4.4__py3-none-any.whl → 0.4.5__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.4.4.dist-info → process_gpt_agent_sdk-0.4.5.dist-info}/METADATA +1 -1
- process_gpt_agent_sdk-0.4.5.dist-info/RECORD +9 -0
- processgpt_agent_sdk/__init__.py +44 -44
- processgpt_agent_sdk/database.py +441 -428
- processgpt_agent_sdk/processgpt_agent_framework.py +497 -494
- processgpt_agent_sdk/utils.py +208 -208
- process_gpt_agent_sdk-0.4.4.dist-info/RECORD +0 -9
- {process_gpt_agent_sdk-0.4.4.dist-info → process_gpt_agent_sdk-0.4.5.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.4.4.dist-info → process_gpt_agent_sdk-0.4.5.dist-info}/top_level.txt +0 -0
|
@@ -1,494 +1,497 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import logging
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import signal
|
|
6
|
-
import uuid
|
|
7
|
-
from typing import Dict, Any, Optional
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
from dotenv import load_dotenv
|
|
11
|
-
|
|
12
|
-
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
13
|
-
from a2a.server.events import EventQueue, Event
|
|
14
|
-
from a2a.types import TaskArtifactUpdateEvent, TaskState, TaskStatusUpdateEvent
|
|
15
|
-
|
|
16
|
-
from .database import (
|
|
17
|
-
initialize_db,
|
|
18
|
-
polling_pending_todos,
|
|
19
|
-
record_events_bulk,
|
|
20
|
-
record_event,
|
|
21
|
-
save_task_result,
|
|
22
|
-
update_task_error,
|
|
23
|
-
get_consumer_id,
|
|
24
|
-
fetch_form_def,
|
|
25
|
-
fetch_users_grouped,
|
|
26
|
-
fetch_email_users_by_proc_inst_id,
|
|
27
|
-
fetch_tenant_mcp,
|
|
28
|
-
)
|
|
29
|
-
from .utils import summarize_error_to_user, summarize_feedback, set_agent_model
|
|
30
|
-
|
|
31
|
-
load_dotenv()
|
|
32
|
-
logging.basicConfig(level=logging.INFO)
|
|
33
|
-
logger = logging.getLogger(__name__)
|
|
34
|
-
|
|
35
|
-
# ------------------------------ 커스텀 예외 ------------------------------
|
|
36
|
-
class ContextPreparationError(Exception):
|
|
37
|
-
"""컨텍스트 준비 실패를 상위 경계에서 단일 처리하기 위한 래퍼 예외."""
|
|
38
|
-
def __init__(self, original: Exception, friendly: Optional[str] = None):
|
|
39
|
-
super().__init__(f"{type(original).__name__}: {str(original)}")
|
|
40
|
-
self.original = original
|
|
41
|
-
self.friendly = friendly
|
|
42
|
-
|
|
43
|
-
# ------------------------------ Event Coalescing (env tunable) ------------------------------
|
|
44
|
-
COALESCE_DELAY = float(os.getenv("EVENT_COALESCE_DELAY_SEC", "1.0")) # 최대 지연
|
|
45
|
-
COALESCE_BATCH = int(os.getenv("EVENT_COALESCE_BATCH", "3")) # 즉시 flush 임계치
|
|
46
|
-
|
|
47
|
-
_EVENT_BUF: list[Dict[str, Any]] = []
|
|
48
|
-
_EVENT_TIMER: Optional[asyncio.TimerHandle] = None
|
|
49
|
-
_EVENT_LOCK = asyncio.Lock()
|
|
50
|
-
|
|
51
|
-
async def _flush_events_now():
|
|
52
|
-
"""버퍼된 이벤트를 bulk RPC로 즉시 저장"""
|
|
53
|
-
global _EVENT_BUF, _EVENT_TIMER
|
|
54
|
-
async with _EVENT_LOCK:
|
|
55
|
-
buf = _EVENT_BUF[:]
|
|
56
|
-
_EVENT_BUF.clear()
|
|
57
|
-
if _EVENT_TIMER and not _EVENT_TIMER.cancelled():
|
|
58
|
-
_EVENT_TIMER.cancel()
|
|
59
|
-
_EVENT_TIMER = None
|
|
60
|
-
if not buf:
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
logger.info("📤 이벤트 버퍼 플러시 시작 - %d개 이벤트", len(buf))
|
|
64
|
-
# 실제 성공/실패 로깅은 record_events_bulk 내부에서 수행
|
|
65
|
-
await record_events_bulk(buf)
|
|
66
|
-
# 여기서는 시도 사실만 남김(성공처럼 보이는 'flushed' 오해 방지)
|
|
67
|
-
logger.info("🔄 이벤트 버퍼 플러시 시도 완료 - %d개 이벤트", len(buf))
|
|
68
|
-
|
|
69
|
-
def _schedule_delayed_flush():
|
|
70
|
-
global _EVENT_TIMER
|
|
71
|
-
if _EVENT_TIMER is None:
|
|
72
|
-
loop = asyncio.get_running_loop()
|
|
73
|
-
_EVENT_TIMER = loop.call_later(COALESCE_DELAY, lambda: asyncio.create_task(_flush_events_now()))
|
|
74
|
-
|
|
75
|
-
async def enqueue_ui_event_coalesced(payload: Dict[str, Any]):
|
|
76
|
-
"""1초 코얼레싱 / COALESCE_BATCH개 모이면 즉시 플러시 (환경변수로 조절 가능)"""
|
|
77
|
-
global _EVENT_BUF
|
|
78
|
-
to_flush_now = False
|
|
79
|
-
async with _EVENT_LOCK:
|
|
80
|
-
_EVENT_BUF.append(payload)
|
|
81
|
-
logger.info("📥 이벤트 버퍼에 추가 - 현재 %d개 (임계치: %d개)", len(_EVENT_BUF), COALESCE_BATCH)
|
|
82
|
-
if len(_EVENT_BUF) >= COALESCE_BATCH:
|
|
83
|
-
to_flush_now = True
|
|
84
|
-
logger.info("⚡ 임계치 도달 - 즉시 플러시 예정")
|
|
85
|
-
else:
|
|
86
|
-
_schedule_delayed_flush()
|
|
87
|
-
logger.info("⏰ 지연 플러시 스케줄링")
|
|
88
|
-
if to_flush_now:
|
|
89
|
-
await _flush_events_now()
|
|
90
|
-
|
|
91
|
-
# ------------------------------ Request Context ------------------------------
|
|
92
|
-
@dataclass
|
|
93
|
-
class TodoListRowContext:
|
|
94
|
-
row: Dict[str, Any]
|
|
95
|
-
|
|
96
|
-
class ProcessGPTRequestContext(RequestContext):
|
|
97
|
-
def __init__(self, row: Dict[str, Any]):
|
|
98
|
-
self.row = row
|
|
99
|
-
self._user_input = (row.get("query") or "").strip()
|
|
100
|
-
self._message = self._user_input
|
|
101
|
-
self._current_task = None
|
|
102
|
-
self._task_state = row.get("draft_status") or ""
|
|
103
|
-
self._extra_context: Dict[str, Any] = {}
|
|
104
|
-
|
|
105
|
-
async def prepare_context(self) -> None:
|
|
106
|
-
"""익스큐터를 위한 컨텍스트 준비를 합니다."""
|
|
107
|
-
|
|
108
|
-
effective_proc_inst_id = self.row.get("root_proc_inst_id") or self.row.get("proc_inst_id")
|
|
109
|
-
tool_val = self.row.get("tool") or ""
|
|
110
|
-
tenant_id = self.row.get("tenant_id") or ""
|
|
111
|
-
user_ids = self.row.get("user_id") or ""
|
|
112
|
-
|
|
113
|
-
try:
|
|
114
|
-
# 데이터베이스 조회
|
|
115
|
-
user_id_list = [u.strip() for u in (user_ids or '').split(',') if u.strip()]
|
|
116
|
-
notify_task = fetch_email_users_by_proc_inst_id(effective_proc_inst_id)
|
|
117
|
-
mcp_task = fetch_tenant_mcp(tenant_id)
|
|
118
|
-
form_task = fetch_form_def(tool_val, tenant_id)
|
|
119
|
-
users_task = fetch_users_grouped(user_id_list)
|
|
120
|
-
|
|
121
|
-
notify_emails, tenant_mcp, form_tuple, users_group = await asyncio.gather(
|
|
122
|
-
notify_task, mcp_task, form_task, users_task
|
|
123
|
-
)
|
|
124
|
-
form_id, form_fields, form_html = form_tuple
|
|
125
|
-
agents, users = users_group
|
|
126
|
-
|
|
127
|
-
# 글로벌 에이전트 모델 설정
|
|
128
|
-
set_agent_model(agents[0] if agents else None)
|
|
129
|
-
|
|
130
|
-
logger.info("\n\n🔍 [데이터베이스 조회 결과]")
|
|
131
|
-
|
|
132
|
-
# Users 정보
|
|
133
|
-
if users:
|
|
134
|
-
user_info = []
|
|
135
|
-
for u in users[:5]:
|
|
136
|
-
name = u.get("name", u.get("username", "Unknown"))
|
|
137
|
-
email = u.get("email", "")
|
|
138
|
-
user_info.append(f"{name}({email})" if email else name)
|
|
139
|
-
logger.info("• Users (%d명): %s%s", len(users), ", ".join(user_info), "..." if len(users) > 5 else "")
|
|
140
|
-
else:
|
|
141
|
-
logger.info("• Users: 없음")
|
|
142
|
-
|
|
143
|
-
# Agents 정보
|
|
144
|
-
if agents:
|
|
145
|
-
agent_info = []
|
|
146
|
-
for a in agents:
|
|
147
|
-
name = a.get("name", a.get("username", "Unknown"))
|
|
148
|
-
tools = a.get("tools", "")
|
|
149
|
-
tool_str = f"[{tools}]" if tools else ""
|
|
150
|
-
agent_info.append(f"{name}{tool_str}")
|
|
151
|
-
logger.info("• Agents (%d개): %s%s", len(agents), ", ".join(agent_info), "..." if len(agents) > 5 else "")
|
|
152
|
-
else:
|
|
153
|
-
logger.info("• Agents: 없음")
|
|
154
|
-
|
|
155
|
-
# Form 정보
|
|
156
|
-
if form_fields:
|
|
157
|
-
pretty_json = json.dumps(form_fields, ensure_ascii=False, separators=(',', ':'))
|
|
158
|
-
logger.info("• Form: %s (%d개 필드) - %s", form_id, len(form_fields), pretty_json)
|
|
159
|
-
else:
|
|
160
|
-
logger.info("• Form: %s (필드 없음)", form_id)
|
|
161
|
-
|
|
162
|
-
# Notify 정보
|
|
163
|
-
if notify_emails:
|
|
164
|
-
email_list = notify_emails.split(',') if ',' in notify_emails else [notify_emails]
|
|
165
|
-
logger.info("• Notify (%d개): %s", len(email_list),
|
|
166
|
-
", ".join(email_list[:3]) + ("..." if len(email_list) > 3 else ""))
|
|
167
|
-
else:
|
|
168
|
-
logger.info("• Notify: 없음")
|
|
169
|
-
|
|
170
|
-
# MCP 정보 - 상세 표시
|
|
171
|
-
if tenant_mcp:
|
|
172
|
-
logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재합니다.", tenant_id)
|
|
173
|
-
else:
|
|
174
|
-
logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재하지 않습니다.", tenant_id)
|
|
175
|
-
|
|
176
|
-
# 피드백 처리
|
|
177
|
-
feedback_data = self.row.get("feedback")
|
|
178
|
-
content_data = self.row.get("output") or self.row.get("draft")
|
|
179
|
-
summarized_feedback = ""
|
|
180
|
-
if feedback_data:
|
|
181
|
-
logger.info("\n\n📝 [피드백 처리]")
|
|
182
|
-
logger.info("• %d자 → AI 요약 중...", len(feedback_data))
|
|
183
|
-
summarized_feedback = await summarize_feedback(feedback_data, content_data)
|
|
184
|
-
logger.info("• 요약 완료: %d자", len(summarized_feedback))
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
logger.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
self.
|
|
353
|
-
self.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
""
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
logger.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
"
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
"
|
|
471
|
-
"
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
13
|
+
from a2a.server.events import EventQueue, Event
|
|
14
|
+
from a2a.types import TaskArtifactUpdateEvent, TaskState, TaskStatusUpdateEvent
|
|
15
|
+
|
|
16
|
+
from .database import (
|
|
17
|
+
initialize_db,
|
|
18
|
+
polling_pending_todos,
|
|
19
|
+
record_events_bulk,
|
|
20
|
+
record_event,
|
|
21
|
+
save_task_result,
|
|
22
|
+
update_task_error,
|
|
23
|
+
get_consumer_id,
|
|
24
|
+
fetch_form_def,
|
|
25
|
+
fetch_users_grouped,
|
|
26
|
+
fetch_email_users_by_proc_inst_id,
|
|
27
|
+
fetch_tenant_mcp,
|
|
28
|
+
)
|
|
29
|
+
from .utils import summarize_error_to_user, summarize_feedback, set_agent_model
|
|
30
|
+
|
|
31
|
+
load_dotenv()
|
|
32
|
+
logging.basicConfig(level=logging.INFO)
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# ------------------------------ 커스텀 예외 ------------------------------
|
|
36
|
+
class ContextPreparationError(Exception):
|
|
37
|
+
"""컨텍스트 준비 실패를 상위 경계에서 단일 처리하기 위한 래퍼 예외."""
|
|
38
|
+
def __init__(self, original: Exception, friendly: Optional[str] = None):
|
|
39
|
+
super().__init__(f"{type(original).__name__}: {str(original)}")
|
|
40
|
+
self.original = original
|
|
41
|
+
self.friendly = friendly
|
|
42
|
+
|
|
43
|
+
# ------------------------------ Event Coalescing (env tunable) ------------------------------
|
|
44
|
+
COALESCE_DELAY = float(os.getenv("EVENT_COALESCE_DELAY_SEC", "1.0")) # 최대 지연
|
|
45
|
+
COALESCE_BATCH = int(os.getenv("EVENT_COALESCE_BATCH", "3")) # 즉시 flush 임계치
|
|
46
|
+
|
|
47
|
+
_EVENT_BUF: list[Dict[str, Any]] = []
|
|
48
|
+
_EVENT_TIMER: Optional[asyncio.TimerHandle] = None
|
|
49
|
+
_EVENT_LOCK = asyncio.Lock()
|
|
50
|
+
|
|
51
|
+
async def _flush_events_now():
|
|
52
|
+
"""버퍼된 이벤트를 bulk RPC로 즉시 저장"""
|
|
53
|
+
global _EVENT_BUF, _EVENT_TIMER
|
|
54
|
+
async with _EVENT_LOCK:
|
|
55
|
+
buf = _EVENT_BUF[:]
|
|
56
|
+
_EVENT_BUF.clear()
|
|
57
|
+
if _EVENT_TIMER and not _EVENT_TIMER.cancelled():
|
|
58
|
+
_EVENT_TIMER.cancel()
|
|
59
|
+
_EVENT_TIMER = None
|
|
60
|
+
if not buf:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
logger.info("📤 이벤트 버퍼 플러시 시작 - %d개 이벤트", len(buf))
|
|
64
|
+
# 실제 성공/실패 로깅은 record_events_bulk 내부에서 수행
|
|
65
|
+
await record_events_bulk(buf)
|
|
66
|
+
# 여기서는 시도 사실만 남김(성공처럼 보이는 'flushed' 오해 방지)
|
|
67
|
+
logger.info("🔄 이벤트 버퍼 플러시 시도 완료 - %d개 이벤트", len(buf))
|
|
68
|
+
|
|
69
|
+
def _schedule_delayed_flush():
|
|
70
|
+
global _EVENT_TIMER
|
|
71
|
+
if _EVENT_TIMER is None:
|
|
72
|
+
loop = asyncio.get_running_loop()
|
|
73
|
+
_EVENT_TIMER = loop.call_later(COALESCE_DELAY, lambda: asyncio.create_task(_flush_events_now()))
|
|
74
|
+
|
|
75
|
+
async def enqueue_ui_event_coalesced(payload: Dict[str, Any]):
|
|
76
|
+
"""1초 코얼레싱 / COALESCE_BATCH개 모이면 즉시 플러시 (환경변수로 조절 가능)"""
|
|
77
|
+
global _EVENT_BUF
|
|
78
|
+
to_flush_now = False
|
|
79
|
+
async with _EVENT_LOCK:
|
|
80
|
+
_EVENT_BUF.append(payload)
|
|
81
|
+
logger.info("📥 이벤트 버퍼에 추가 - 현재 %d개 (임계치: %d개)", len(_EVENT_BUF), COALESCE_BATCH)
|
|
82
|
+
if len(_EVENT_BUF) >= COALESCE_BATCH:
|
|
83
|
+
to_flush_now = True
|
|
84
|
+
logger.info("⚡ 임계치 도달 - 즉시 플러시 예정")
|
|
85
|
+
else:
|
|
86
|
+
_schedule_delayed_flush()
|
|
87
|
+
logger.info("⏰ 지연 플러시 스케줄링")
|
|
88
|
+
if to_flush_now:
|
|
89
|
+
await _flush_events_now()
|
|
90
|
+
|
|
91
|
+
# ------------------------------ Request Context ------------------------------
|
|
92
|
+
@dataclass
|
|
93
|
+
class TodoListRowContext:
|
|
94
|
+
row: Dict[str, Any]
|
|
95
|
+
|
|
96
|
+
class ProcessGPTRequestContext(RequestContext):
|
|
97
|
+
def __init__(self, row: Dict[str, Any]):
|
|
98
|
+
self.row = row
|
|
99
|
+
self._user_input = (row.get("query") or "").strip()
|
|
100
|
+
self._message = self._user_input
|
|
101
|
+
self._current_task = None
|
|
102
|
+
self._task_state = row.get("draft_status") or ""
|
|
103
|
+
self._extra_context: Dict[str, Any] = {}
|
|
104
|
+
|
|
105
|
+
async def prepare_context(self) -> None:
|
|
106
|
+
"""익스큐터를 위한 컨텍스트 준비를 합니다."""
|
|
107
|
+
|
|
108
|
+
effective_proc_inst_id = self.row.get("root_proc_inst_id") or self.row.get("proc_inst_id")
|
|
109
|
+
tool_val = self.row.get("tool") or ""
|
|
110
|
+
tenant_id = self.row.get("tenant_id") or ""
|
|
111
|
+
user_ids = self.row.get("user_id") or ""
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# 데이터베이스 조회
|
|
115
|
+
user_id_list = [u.strip() for u in (user_ids or '').split(',') if u.strip()]
|
|
116
|
+
notify_task = fetch_email_users_by_proc_inst_id(effective_proc_inst_id)
|
|
117
|
+
mcp_task = fetch_tenant_mcp(tenant_id)
|
|
118
|
+
form_task = fetch_form_def(tool_val, tenant_id)
|
|
119
|
+
users_task = fetch_users_grouped(user_id_list)
|
|
120
|
+
|
|
121
|
+
notify_emails, tenant_mcp, form_tuple, users_group = await asyncio.gather(
|
|
122
|
+
notify_task, mcp_task, form_task, users_task
|
|
123
|
+
)
|
|
124
|
+
form_id, form_fields, form_html = form_tuple
|
|
125
|
+
agents, users = users_group
|
|
126
|
+
|
|
127
|
+
# 글로벌 에이전트 모델 설정
|
|
128
|
+
set_agent_model(agents[0] if agents else None)
|
|
129
|
+
|
|
130
|
+
logger.info("\n\n🔍 [데이터베이스 조회 결과]")
|
|
131
|
+
|
|
132
|
+
# Users 정보
|
|
133
|
+
if users:
|
|
134
|
+
user_info = []
|
|
135
|
+
for u in users[:5]:
|
|
136
|
+
name = u.get("name", u.get("username", "Unknown"))
|
|
137
|
+
email = u.get("email", "")
|
|
138
|
+
user_info.append(f"{name}({email})" if email else name)
|
|
139
|
+
logger.info("• Users (%d명): %s%s", len(users), ", ".join(user_info), "..." if len(users) > 5 else "")
|
|
140
|
+
else:
|
|
141
|
+
logger.info("• Users: 없음")
|
|
142
|
+
|
|
143
|
+
# Agents 정보
|
|
144
|
+
if agents:
|
|
145
|
+
agent_info = []
|
|
146
|
+
for a in agents:
|
|
147
|
+
name = a.get("name", a.get("username", "Unknown"))
|
|
148
|
+
tools = a.get("tools", "")
|
|
149
|
+
tool_str = f"[{tools}]" if tools else ""
|
|
150
|
+
agent_info.append(f"{name}{tool_str}")
|
|
151
|
+
logger.info("• Agents (%d개): %s%s", len(agents), ", ".join(agent_info), "..." if len(agents) > 5 else "")
|
|
152
|
+
else:
|
|
153
|
+
logger.info("• Agents: 없음")
|
|
154
|
+
|
|
155
|
+
# Form 정보
|
|
156
|
+
if form_fields:
|
|
157
|
+
pretty_json = json.dumps(form_fields, ensure_ascii=False, separators=(',', ':'))
|
|
158
|
+
logger.info("• Form: %s (%d개 필드) - %s", form_id, len(form_fields), pretty_json)
|
|
159
|
+
else:
|
|
160
|
+
logger.info("• Form: %s (필드 없음)", form_id)
|
|
161
|
+
|
|
162
|
+
# Notify 정보
|
|
163
|
+
if notify_emails:
|
|
164
|
+
email_list = notify_emails.split(',') if ',' in notify_emails else [notify_emails]
|
|
165
|
+
logger.info("• Notify (%d개): %s", len(email_list),
|
|
166
|
+
", ".join(email_list[:3]) + ("..." if len(email_list) > 3 else ""))
|
|
167
|
+
else:
|
|
168
|
+
logger.info("• Notify: 없음")
|
|
169
|
+
|
|
170
|
+
# MCP 정보 - 상세 표시
|
|
171
|
+
if tenant_mcp:
|
|
172
|
+
logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재합니다.", tenant_id)
|
|
173
|
+
else:
|
|
174
|
+
logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재하지 않습니다.", tenant_id)
|
|
175
|
+
|
|
176
|
+
# 피드백 처리
|
|
177
|
+
feedback_data = self.row.get("feedback")
|
|
178
|
+
content_data = self.row.get("output") or self.row.get("draft")
|
|
179
|
+
summarized_feedback = ""
|
|
180
|
+
if feedback_data:
|
|
181
|
+
logger.info("\n\n📝 [피드백 처리]")
|
|
182
|
+
logger.info("• %d자 → AI 요약 중...", len(feedback_data))
|
|
183
|
+
summarized_feedback = await summarize_feedback(feedback_data, content_data)
|
|
184
|
+
logger.info("• 요약 완료: %d자", len(summarized_feedback))
|
|
185
|
+
|
|
186
|
+
logger.info("sensitive_data: %s", self.row.get("sensitive_data") or "{}")
|
|
187
|
+
|
|
188
|
+
# 컨텍스트 구성
|
|
189
|
+
self._extra_context = {
|
|
190
|
+
"id": self.row.get("id"),
|
|
191
|
+
"proc_inst_id": effective_proc_inst_id,
|
|
192
|
+
"root_proc_inst_id": self.row.get("root_proc_inst_id"),
|
|
193
|
+
"activity_name": self.row.get("activity_name"),
|
|
194
|
+
"agents": agents,
|
|
195
|
+
"users": users,
|
|
196
|
+
"tenant_mcp": tenant_mcp,
|
|
197
|
+
"form_fields": form_fields,
|
|
198
|
+
"form_html": form_html,
|
|
199
|
+
"form_id": form_id,
|
|
200
|
+
"notify_user_emails": notify_emails,
|
|
201
|
+
"summarized_feedback": summarized_feedback,
|
|
202
|
+
"sensitive_data": self.row.get("sensitive_data") or "{}",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.info("\n\n🎉 [컨텍스트 준비 완료] 모든 데이터 준비됨")
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error("❌ [데이터 조회 실패] %s", str(e))
|
|
209
|
+
raise ContextPreparationError(e)
|
|
210
|
+
|
|
211
|
+
def get_user_input(self) -> str:
|
|
212
|
+
return self._user_input
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def message(self) -> str:
|
|
216
|
+
return self._message
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def current_task(self):
|
|
220
|
+
return self._current_task
|
|
221
|
+
|
|
222
|
+
@current_task.setter
|
|
223
|
+
def current_task(self, task):
|
|
224
|
+
self._current_task = task
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def task_state(self) -> str:
|
|
228
|
+
return self._task_state
|
|
229
|
+
|
|
230
|
+
def get_context_data(self) -> Dict[str, Any]:
|
|
231
|
+
return {"row": self.row, "extras": self._extra_context}
|
|
232
|
+
|
|
233
|
+
# ------------------------------ Event Queue ------------------------------
|
|
234
|
+
class ProcessGPTEventQueue(EventQueue):
|
|
235
|
+
def __init__(self, todolist_id: str, agent_orch: str, proc_inst_id: Optional[str]):
|
|
236
|
+
self.todolist_id = todolist_id
|
|
237
|
+
self.agent_orch = agent_orch
|
|
238
|
+
self.proc_inst_id = proc_inst_id
|
|
239
|
+
super().__init__()
|
|
240
|
+
|
|
241
|
+
def enqueue_event(self, event: Event):
|
|
242
|
+
try:
|
|
243
|
+
proc_inst_id_val = getattr(event, "contextId", None) or self.proc_inst_id
|
|
244
|
+
todo_id_val = getattr(event, "taskId", None) or str(self.todolist_id)
|
|
245
|
+
logger.info("\n\n📨 이벤트 수신: %s (task=%s)", type(event).__name__, self.todolist_id)
|
|
246
|
+
|
|
247
|
+
# 1) 결과물 저장
|
|
248
|
+
if isinstance(event, TaskArtifactUpdateEvent):
|
|
249
|
+
logger.info("📄 아티팩트 업데이트 이벤트 처리 중...")
|
|
250
|
+
is_final = bool(
|
|
251
|
+
getattr(event, "final", None)
|
|
252
|
+
or getattr(event, "lastChunk", None)
|
|
253
|
+
or getattr(event, "last_chunk", None)
|
|
254
|
+
or getattr(event, "last", None)
|
|
255
|
+
)
|
|
256
|
+
artifact_content = self._extract_payload(event)
|
|
257
|
+
logger.info("💾 아티팩트 저장 중... (final=%s)", is_final)
|
|
258
|
+
asyncio.create_task(save_task_result(self.todolist_id, artifact_content, is_final))
|
|
259
|
+
logger.info("✅ 아티팩트 저장 완료")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# 2) 상태 이벤트 저장(코얼레싱 → bulk)
|
|
263
|
+
if isinstance(event, TaskStatusUpdateEvent):
|
|
264
|
+
logger.info("📊 상태 업데이트 이벤트 처리 중...")
|
|
265
|
+
metadata = getattr(event, "metadata", None) or {}
|
|
266
|
+
crew_type_val = metadata.get("crew_type")
|
|
267
|
+
status_obj = getattr(event, "status", None)
|
|
268
|
+
state_val = getattr(status_obj, "state", None)
|
|
269
|
+
event_type_val = {TaskState.input_required: "human_asked"}.get(state_val) or metadata.get("event_type")
|
|
270
|
+
status_val = metadata.get("status")
|
|
271
|
+
job_id_val = metadata.get("job_id")
|
|
272
|
+
|
|
273
|
+
logger.info("🔍 이벤트 메타데이터 분석 - event_type: %s, status: %s", event_type_val, status_val)
|
|
274
|
+
|
|
275
|
+
payload: Dict[str, Any] = {
|
|
276
|
+
"id": str(uuid.uuid4()),
|
|
277
|
+
"job_id": job_id_val,
|
|
278
|
+
"todo_id": str(todo_id_val),
|
|
279
|
+
"proc_inst_id": proc_inst_id_val,
|
|
280
|
+
"crew_type": crew_type_val,
|
|
281
|
+
"event_type": event_type_val,
|
|
282
|
+
"data": self._extract_payload(event),
|
|
283
|
+
"status": status_val or None,
|
|
284
|
+
}
|
|
285
|
+
logger.info("📤 상태 이벤트 큐에 추가 중...")
|
|
286
|
+
asyncio.create_task(enqueue_ui_event_coalesced(payload))
|
|
287
|
+
logger.info("✅ 상태 이벤트 큐 추가 완료")
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error("❌ 이벤트 처리 실패: %s", str(e))
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
def _extract_payload(self, event: Event) -> Any:
|
|
295
|
+
artifact_or_none = getattr(event, "artifact", None)
|
|
296
|
+
status_or_none = getattr(event, "status", None)
|
|
297
|
+
message_or_none = getattr(status_or_none, "message", None)
|
|
298
|
+
source = artifact_or_none if artifact_or_none is not None else message_or_none
|
|
299
|
+
return self._parse_json_or_text(source)
|
|
300
|
+
|
|
301
|
+
def _parse_json_or_text(self, value: Any) -> Any:
|
|
302
|
+
if value is None:
|
|
303
|
+
return {}
|
|
304
|
+
if isinstance(value, str):
|
|
305
|
+
text = value.strip()
|
|
306
|
+
if not text:
|
|
307
|
+
return ""
|
|
308
|
+
return json.loads(text)
|
|
309
|
+
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
|
|
310
|
+
value = value.model_dump()
|
|
311
|
+
elif not isinstance(value, dict) and hasattr(value, "dict") and callable(getattr(value, "dict")):
|
|
312
|
+
value = value.dict()
|
|
313
|
+
elif not isinstance(value, dict) and hasattr(value, "__dict__"):
|
|
314
|
+
value = value.__dict__
|
|
315
|
+
if isinstance(value, dict):
|
|
316
|
+
parts = value.get("parts")
|
|
317
|
+
if isinstance(parts, list) and parts:
|
|
318
|
+
first = parts[0] if isinstance(parts[0], dict) else None
|
|
319
|
+
if first and isinstance(first, dict):
|
|
320
|
+
txt = first.get("text") or first.get("content") or first.get("data")
|
|
321
|
+
if isinstance(txt, str):
|
|
322
|
+
return json.loads(txt)
|
|
323
|
+
top_text = value.get("text") or value.get("content") or value.get("data")
|
|
324
|
+
if isinstance(top_text, str):
|
|
325
|
+
return json.loads(top_text)
|
|
326
|
+
return value
|
|
327
|
+
return value
|
|
328
|
+
|
|
329
|
+
def task_done(self) -> None:
|
|
330
|
+
try:
|
|
331
|
+
logger.info("🏁 작업 완료 이벤트 생성 중...")
|
|
332
|
+
payload: Dict[str, Any] = {
|
|
333
|
+
"id": str(uuid.uuid4()),
|
|
334
|
+
"job_id": "CREW_FINISHED",
|
|
335
|
+
"todo_id": str(self.todolist_id),
|
|
336
|
+
"proc_inst_id": self.proc_inst_id,
|
|
337
|
+
"crew_type": "crew",
|
|
338
|
+
"data": "Task completed successfully",
|
|
339
|
+
"event_type": "crew_completed",
|
|
340
|
+
"status": None,
|
|
341
|
+
}
|
|
342
|
+
logger.info("📤 작업 완료 이벤트 큐에 추가 중...")
|
|
343
|
+
asyncio.create_task(enqueue_ui_event_coalesced(payload))
|
|
344
|
+
logger.info("✅ 작업 완료 이벤트 기록 완료")
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error("❌ 작업 완료 이벤트 기록 실패: %s", str(e))
|
|
347
|
+
raise
|
|
348
|
+
|
|
349
|
+
# ------------------------------ Agent Server ------------------------------
|
|
350
|
+
class ProcessGPTAgentServer:
|
|
351
|
+
def __init__(self, agent_executor: AgentExecutor, agent_type: str):
|
|
352
|
+
self.agent_executor = agent_executor
|
|
353
|
+
self.agent_orch = agent_type
|
|
354
|
+
self.is_running = False
|
|
355
|
+
self._shutdown_event = asyncio.Event()
|
|
356
|
+
self._current_todo_id: Optional[str] = None # 진행 중 작업 추적(참고용)
|
|
357
|
+
|
|
358
|
+
async def _install_signal_handlers(self):
|
|
359
|
+
loop = asyncio.get_running_loop()
|
|
360
|
+
try:
|
|
361
|
+
loop.add_signal_handler(signal.SIGTERM, lambda: self._shutdown_event.set())
|
|
362
|
+
loop.add_signal_handler(signal.SIGINT, lambda: self._shutdown_event.set())
|
|
363
|
+
except NotImplementedError:
|
|
364
|
+
# Windows 등 일부 환경은 지원 안 됨
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
async def run(self):
|
|
368
|
+
self.is_running = True
|
|
369
|
+
logger.info("\n\n🚀 ProcessGPT Agent Server START (agent=%s)\n", self.agent_orch)
|
|
370
|
+
initialize_db()
|
|
371
|
+
await self._install_signal_handlers()
|
|
372
|
+
|
|
373
|
+
while self.is_running and not self._shutdown_event.is_set():
|
|
374
|
+
try:
|
|
375
|
+
row = await polling_pending_todos(self.agent_orch, get_consumer_id())
|
|
376
|
+
|
|
377
|
+
if row:
|
|
378
|
+
logger.info("✅ [새 작업 발견] Task ID: %s", row.get("id"))
|
|
379
|
+
logger.info("• Activity: %s | Tool: %s | Tenant: %s",
|
|
380
|
+
row.get("activity_name"), row.get("tool"), row.get("tenant_id"))
|
|
381
|
+
try:
|
|
382
|
+
self._current_todo_id = str(row.get("id"))
|
|
383
|
+
await self.process_todolist_item(row)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
# 경계에서 처리(에러 이벤트 + FAILED 마킹) 후 예외 재전달됨.
|
|
386
|
+
logger.exception("process_todolist_item failed: %s", str(e))
|
|
387
|
+
finally:
|
|
388
|
+
self._current_todo_id = None
|
|
389
|
+
# 작업이 있었으므로 슬립 생략 → 즉시 다음 폴링
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
# 작업 없을 때만 10초 대기
|
|
393
|
+
await asyncio.sleep(10)
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
# 폴링 자체 오류는 특정 작업에 귀속되지 않으므로 상태 마킹 대상 없음
|
|
397
|
+
logger.exception("run loop error: %s", str(e))
|
|
398
|
+
await asyncio.sleep(10)
|
|
399
|
+
|
|
400
|
+
# 종료 시 남은 이벤트 강제 flush (오류로 간주하지 않음)
|
|
401
|
+
try:
|
|
402
|
+
await _flush_events_now()
|
|
403
|
+
logger.info("🧹 graceful shutdown: pending events flushed")
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.exception("flush on shutdown failed: %s", str(e))
|
|
406
|
+
|
|
407
|
+
logger.info("👋 Agent server stopped.")
|
|
408
|
+
|
|
409
|
+
async def process_todolist_item(self, row: Dict[str, Any]):
|
|
410
|
+
"""
|
|
411
|
+
경계 정책(최종본):
|
|
412
|
+
- 어떤 예외든 여기에서 잡힘
|
|
413
|
+
- 항상 단일 경로로:
|
|
414
|
+
1) 사용자 친화 5줄 설명 생성
|
|
415
|
+
2) event_type='error' 단건 이벤트 기록
|
|
416
|
+
3) todolist를 FAILED로 마킹
|
|
417
|
+
4) 예외 재전달(상위 루프는 죽지 않고 다음 폴링)
|
|
418
|
+
"""
|
|
419
|
+
task_id = row.get("id")
|
|
420
|
+
logger.info("\n🎯 [작업 처리 시작] Task ID: %s", task_id)
|
|
421
|
+
|
|
422
|
+
friendly_text: Optional[str] = None
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
# 1) 컨텍스트 준비 (실패 시 ContextPreparationError로 올라옴)
|
|
426
|
+
context = ProcessGPTRequestContext(row)
|
|
427
|
+
await context.prepare_context()
|
|
428
|
+
|
|
429
|
+
# 2) 실행
|
|
430
|
+
logger.info("\n\n🤖 [Agent Orchestrator 실행]")
|
|
431
|
+
event_queue = ProcessGPTEventQueue(str(task_id), self.agent_orch, row.get("proc_inst_id"))
|
|
432
|
+
await self.agent_executor.execute(context, event_queue)
|
|
433
|
+
event_queue.task_done()
|
|
434
|
+
logger.info("\n\n🎉 [Agent Orchestrator 완료] Task ID: %s", task_id)
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error("❌ 작업 처리 중 오류 발생: %s", str(e))
|
|
438
|
+
|
|
439
|
+
# 컨텍스트 실패라면 friendly가 없을 수 있어, 여기서 반드시 생성
|
|
440
|
+
try:
|
|
441
|
+
logger.info("📝 사용자 친화 오류 메시지 생성 중...")
|
|
442
|
+
if isinstance(e, ContextPreparationError) and e.friendly:
|
|
443
|
+
friendly_text = e.friendly
|
|
444
|
+
else:
|
|
445
|
+
friendly_text = await summarize_error_to_user(
|
|
446
|
+
e if not isinstance(e, ContextPreparationError) else e.original,
|
|
447
|
+
{
|
|
448
|
+
"task_id": task_id,
|
|
449
|
+
"proc_inst_id": row.get("proc_inst_id"),
|
|
450
|
+
"agent_orch": self.agent_orch,
|
|
451
|
+
"tool": row.get("tool"),
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
logger.info("✅ 사용자 친화 오류 메시지 생성 완료")
|
|
455
|
+
except Exception:
|
|
456
|
+
logger.warning("⚠️ 사용자 친화 오류 메시지 생성 실패")
|
|
457
|
+
# 요약 생성 실패 시에도 처리 계속
|
|
458
|
+
friendly_text = None
|
|
459
|
+
|
|
460
|
+
# 에러 이벤트 기록(단건). 실패해도 로그만 남기고 진행.
|
|
461
|
+
logger.info("📤 오류 이벤트 기록 중...")
|
|
462
|
+
payload: Dict[str, Any] = {
|
|
463
|
+
"id": str(uuid.uuid4()),
|
|
464
|
+
"job_id": "TASK_ERROR",
|
|
465
|
+
"todo_id": str(task_id),
|
|
466
|
+
"proc_inst_id": row.get("proc_inst_id"),
|
|
467
|
+
"crew_type": "agent",
|
|
468
|
+
"event_type": "error",
|
|
469
|
+
"data": {
|
|
470
|
+
"name": "시스템 오류 알림",
|
|
471
|
+
"goal": "오류 원인과 대처 안내를 전달합니다.",
|
|
472
|
+
"agent_profile": "/images/chat-icon.png",
|
|
473
|
+
"friendly": friendly_text or "처리 중 오류가 발생했습니다. 로그를 확인해 주세요.",
|
|
474
|
+
"raw_error": f"{type(e).__name__}: {str(e)}" if not isinstance(e, ContextPreparationError) else f"{type(e.original).__name__}: {str(e.original)}",
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
try:
|
|
478
|
+
asyncio.create_task(record_event(payload))
|
|
479
|
+
logger.info("✅ 오류 이벤트 기록 완료")
|
|
480
|
+
except Exception:
|
|
481
|
+
logger.exception("❌ 오류 이벤트 기록 실패")
|
|
482
|
+
|
|
483
|
+
# 상태 FAILED 마킹
|
|
484
|
+
logger.info("🏷️ 작업 상태 FAILED로 마킹 중...")
|
|
485
|
+
try:
|
|
486
|
+
await update_task_error(str(task_id))
|
|
487
|
+
logger.info("✅ 작업 상태 FAILED 마킹 완료")
|
|
488
|
+
except Exception:
|
|
489
|
+
logger.exception("❌ 작업 상태 FAILED 마킹 실패")
|
|
490
|
+
|
|
491
|
+
# 상위로 재전달하여 루프는 계속(죽지 않음)
|
|
492
|
+
logger.error("🔄 오류 처리 완료 - 다음 작업으로 계속 진행")
|
|
493
|
+
|
|
494
|
+
def stop(self):
|
|
495
|
+
self.is_running = False
|
|
496
|
+
self._shutdown_event.set()
|
|
497
|
+
logger.info("ProcessGPT Agent Server stopping...")
|