process-gpt-agent-sdk 0.2.11__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.

@@ -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")