process-gpt-agent-sdk 0.3.17__tar.gz → 0.3.19__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-gpt-agent-sdk
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/your-org/process-gpt-agent-sdk
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-gpt-agent-sdk
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/your-org/process-gpt-agent-sdk
@@ -0,0 +1,43 @@
1
+ from .processgpt_agent_framework import (
2
+ ProcessGPTAgentServer,
3
+ ProcessGPTRequestContext,
4
+ ProcessGPTEventQueue,
5
+ ContextPreparationError,
6
+ )
7
+ from .database import (
8
+ initialize_db,
9
+ get_consumer_id,
10
+ polling_pending_todos,
11
+ record_event,
12
+ record_events_bulk,
13
+ save_task_result,
14
+ update_task_error,
15
+ fetch_form_def,
16
+ fetch_users_grouped,
17
+ fetch_email_users_by_proc_inst_id,
18
+ fetch_tenant_mcp,
19
+ )
20
+ from .utils import (
21
+ summarize_error_to_user,
22
+ summarize_feedback,
23
+ )
24
+
25
+ __all__ = [
26
+ "ProcessGPTAgentServer",
27
+ "ProcessGPTRequestContext",
28
+ "ProcessGPTEventQueue",
29
+ "ContextPreparationError",
30
+ "initialize_db",
31
+ "get_consumer_id",
32
+ "polling_pending_todos",
33
+ "record_event",
34
+ "record_events_bulk",
35
+ "save_task_result",
36
+ "update_task_error",
37
+ "fetch_form_def",
38
+ "fetch_users_grouped",
39
+ "fetch_email_users_by_proc_inst_id",
40
+ "fetch_tenant_mcp",
41
+ "summarize_error_to_user",
42
+ "summarize_feedback",
43
+ ]
@@ -130,51 +130,27 @@ async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict
130
130
  ).execute()
131
131
 
132
132
  rows = resp.data or []
133
- return rows[0] if rows else None
133
+ if not rows:
134
+ return None
135
+
136
+ row = rows[0]
137
+ # 빈 값들을 NULL로 변환
138
+ if row.get("feedback") in ([], {}):
139
+ row["feedback"] = None
140
+ if row.get("output") in ([], {}):
141
+ row["output"] = None
142
+ if row.get("draft") in ([], {}):
143
+ row["draft"] = None
144
+
145
+ return row
134
146
 
135
147
  return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
136
148
 
137
- # ------------------------------ Context Bundle ------------------------------
138
- async def fetch_context_bundle(
139
- proc_inst_id: str,
140
- tenant_id: str,
141
- tool_val: str,
142
- user_ids: str,
143
- ) -> Tuple[str, Optional[Dict[str, Any]], Tuple[Optional[str], List[Dict[str, Any]], Optional[str]], List[Dict[str, Any]]]:
144
- def _call():
145
- client = get_db_client()
146
- resp = client.rpc(
147
- "fetch_context_bundle",
148
- {
149
- "p_proc_inst_id": proc_inst_id or "",
150
- "p_tenant_id": tenant_id or "",
151
- "p_tool": tool_val or "",
152
- "p_user_ids": user_ids or "",
153
- },
154
- ).execute()
155
- rows = resp.data or []
156
- row = rows[0] if rows else {}
157
- notify = (row.get("notify_emails") or "").strip()
158
- mcp = row.get("tenant_mcp") or None
159
- form_id = row.get("form_id")
160
- form_fields = row.get("form_fields") or [{"key": form_id, "type": "default", "text": ""}]
161
- form_html = row.get("form_html")
162
- agents = row.get("agents") or []
163
- return notify, mcp, (form_id, form_fields, form_html), agents
164
-
165
- try:
166
- return await _async_retry(_call, name="fetch_context_bundle", fallback=lambda: ("", None, (None, [], None), []))
167
- except Exception as e:
168
- logger.error("fetch_context_bundle fatal: %s", str(e), exc_info=e)
169
- return ("", None, (None, [], None), [])
170
149
 
171
150
  # ------------------------------ Events & Results ------------------------------
172
151
  async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
173
- """
174
- 이벤트 다건 저장:
175
- - 성공: 'record_events_bulk ok'
176
- - 실패(최종): '❌ record_events_bulk failed' (개수 포함)
177
- """
152
+ """이벤트 다건 저장 함수"""
153
+
178
154
  if not payloads:
179
155
  return
180
156
 
@@ -196,12 +172,8 @@ async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
196
172
  logger.info("record_events_bulk ok: count=%d", len(safe_list))
197
173
 
198
174
  async def record_event(payload: Dict[str, Any]) -> None:
199
- """
200
- 단건 이벤트 저장.
201
- - 성공: 'record_event ok'
202
- - 실패(최종): '❌ record_event failed'
203
- - 실패해도 워크플로우는 계속(요청사항)
204
- """
175
+ """단건 이벤트 저장 함수"""
176
+
205
177
  if not payload:
206
178
  return
207
179
 
@@ -219,6 +191,8 @@ async def record_event(payload: Dict[str, Any]) -> None:
219
191
  logger.info("record_event ok: event_type=%s", payload.get("event_type"))
220
192
 
221
193
  async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
194
+ """결과 저장 함수"""
195
+
222
196
  if not todo_id:
223
197
  logger.error("save_task_result invalid todo_id: %s", str(todo_id))
224
198
  return
@@ -245,6 +219,8 @@ async def save_task_result(todo_id: str, result: Any, final: bool = False) -> No
245
219
 
246
220
  # ------------------------------ Failure Status ------------------------------
247
221
  async def update_task_error(todo_id: str) -> None:
222
+ """작업 실패 상태 업데이트 함수"""
223
+
248
224
  if not todo_id:
249
225
  return
250
226
 
@@ -257,3 +233,151 @@ async def update_task_error(todo_id: str) -> None:
257
233
  logger.error("❌ update_task_error failed todo_id=%s", todo_id)
258
234
  else:
259
235
  logger.info("update_task_error ok todo_id=%s", todo_id)
236
+
237
+ # ============================== Prepare Context ==============================
238
+
239
+ from typing import Any, Dict, List, Optional, Tuple
240
+
241
+ async def fetch_form_def(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
242
+ """폼 정의 조회 함수"""
243
+ form_id = (tool_val or "").replace("formHandler:", "", 1)
244
+
245
+ def _call():
246
+ client = get_db_client()
247
+ resp = (
248
+ client.table("form_def")
249
+ .select("fields_json, html")
250
+ .eq("id", form_id)
251
+ .eq("tenant_id", tenant_id or "")
252
+ .execute()
253
+ )
254
+ data = (resp.data or [])
255
+ if not data:
256
+ return None
257
+
258
+ row = data[0]
259
+ return {
260
+ "fields": row.get("fields_json"),
261
+ "html": row.get("html"),
262
+ }
263
+
264
+ try:
265
+ res = await _async_retry(_call, name="fetch_form_def")
266
+ except Exception as e:
267
+ logger.error("fetch_form_def fatal: %s", str(e), exc_info=e)
268
+ res = None
269
+
270
+ if not res or not res.get("fields"):
271
+ # 기본(자유형식) 폼
272
+ return (
273
+ form_id or "freeform",
274
+ [{"key": "freeform", "type": "textarea", "text": "자유형식 입력", "placeholder": "원하는 내용을 자유롭게 입력해주세요."}],
275
+ None,
276
+ )
277
+ return (form_id or "freeform", res["fields"], res.get("html"))
278
+
279
+
280
+ async def fetch_users_grouped(user_ids: List[str]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
281
+ """해당 todo에서 사용자 목록과 에이전트 목록 조회하는는 함수"""
282
+ ids = [u for u in (user_ids or []) if u]
283
+ if not ids:
284
+ return ([], [])
285
+
286
+ def _call():
287
+ client = get_db_client()
288
+ resp = (
289
+ client.table("users")
290
+ .select("*")
291
+ .in_("id", ids)
292
+ .execute()
293
+ )
294
+ rows = resp.data or []
295
+ return rows
296
+
297
+ try:
298
+ rows = await _async_retry(_call, name="fetch_users_grouped", fallback=lambda: [])
299
+ except Exception as e:
300
+ logger.error("fetch_users_grouped fatal: %s", str(e), exc_info=e)
301
+ rows = []
302
+
303
+ agents, users = [], []
304
+ for r in rows:
305
+ if r.get("is_agent") is True:
306
+ agents.append(r)
307
+ else:
308
+ users.append(r)
309
+ return (agents, users)
310
+
311
+ async def fetch_email_users_by_proc_inst_id(proc_inst_id: str) -> str:
312
+ """proc_inst_id로 이메일 수집(사람만): todolist → users(in) 한 번에"""
313
+ if not proc_inst_id:
314
+ return ""
315
+
316
+ def _call():
317
+ client = get_db_client()
318
+ # 3-1) 해당 인스턴스의 user_id 수집(중복 제거)
319
+ tl = (
320
+ client.table("todolist")
321
+ .select("user_id")
322
+ .eq("proc_inst_id", proc_inst_id)
323
+ .execute()
324
+ )
325
+ ids_set = set()
326
+ for row in (tl.data or []):
327
+ uid_csv = (row.get("user_id") or "").strip()
328
+ if not uid_csv:
329
+ continue
330
+ # user_id는 문자열 CSV라고 전제
331
+ for uid in uid_csv.split(","):
332
+ u = uid.strip()
333
+ if u:
334
+ ids_set.add(u)
335
+ if not ids_set:
336
+ return []
337
+
338
+ # 3-2) 한 번의 IN 조회로 사람만 이메일 추출
339
+ ur = (
340
+ client.table("users")
341
+ .select("id, email, is_agent")
342
+ .in_("id", list(ids_set))
343
+ .eq("is_agent", False)
344
+ .execute()
345
+ )
346
+ emails = []
347
+ for u in (ur.data or []):
348
+ email = (u.get("email") or "").strip()
349
+ if email:
350
+ emails.append(email)
351
+ # 중복 제거 및 정렬(보기 좋게)
352
+ return sorted(set(emails))
353
+
354
+ try:
355
+ emails = await _async_retry(_call, name="fetch_email_users_by_proc_inst_id", fallback=lambda: [])
356
+ except Exception as e:
357
+ logger.error("fetch_email_users_by_proc_inst_id fatal: %s", str(e), exc_info=e)
358
+ emails = []
359
+
360
+ return ",".join(emails) if emails else ""
361
+
362
+ async def fetch_tenant_mcp(tenant_id: str) -> Optional[Dict[str, Any]]:
363
+ """mcp 설정 조회 함수"""
364
+ if not tenant_id:
365
+ return None
366
+
367
+ def _call():
368
+ client = get_db_client()
369
+ return (
370
+ client.table("tenants")
371
+ .select("mcp")
372
+ .eq("id", tenant_id)
373
+ .single()
374
+ .execute()
375
+ )
376
+
377
+ try:
378
+ resp = await _async_retry(_call, name="fetch_tenant_mcp", fallback=lambda: None)
379
+ except Exception as e:
380
+ logger.error("fetch_tenant_mcp fatal: %s", str(e), exc_info=e)
381
+ return None
382
+
383
+ return resp.data.get("mcp") if resp and getattr(resp, "data", None) else None
@@ -17,13 +17,16 @@ from .database import (
17
17
  initialize_db,
18
18
  polling_pending_todos,
19
19
  record_events_bulk,
20
- record_event, # 단건 이벤트 기록
20
+ record_event,
21
21
  save_task_result,
22
22
  update_task_error,
23
23
  get_consumer_id,
24
- fetch_context_bundle,
24
+ fetch_form_def,
25
+ fetch_users_grouped,
26
+ fetch_email_users_by_proc_inst_id,
27
+ fetch_tenant_mcp,
25
28
  )
26
- from .utils import summarize_error_to_user
29
+ from .utils import summarize_error_to_user, summarize_feedback
27
30
 
28
31
  load_dotenv()
29
32
  logging.basicConfig(level=logging.INFO)
@@ -93,64 +96,116 @@ class TodoListRowContext:
93
96
  class ProcessGPTRequestContext(RequestContext):
94
97
  def __init__(self, row: Dict[str, Any]):
95
98
  self.row = row
96
- self._user_input = (row.get("description") or "").strip()
99
+ self._user_input = (row.get("query") or "").strip()
97
100
  self._message = self._user_input
98
101
  self._current_task = None
99
102
  self._task_state = row.get("draft_status") or ""
100
103
  self._extra_context: Dict[str, Any] = {}
101
104
 
102
105
  async def prepare_context(self) -> None:
103
- """
104
- 컨텍스트 준비.
105
- - 실패 시: 더 이상 진행하지 않고 ContextPreparationError를 발생시켜
106
- 상위 경계에서 FAILED 처리(이벤트 기록 포함)를 단일 경로로 수행.
107
- """
108
- logger.info("\n🔧 컨텍스트 준비 시작...")
109
-
110
- # 1단계: 기본 정보 추출
106
+ """익스큐터를 위한 컨텍스트 준비를 합니다."""
107
+
111
108
  effective_proc_inst_id = self.row.get("root_proc_inst_id") or self.row.get("proc_inst_id")
112
109
  tool_val = self.row.get("tool") or ""
113
110
  tenant_id = self.row.get("tenant_id") or ""
114
111
  user_ids = self.row.get("user_id") or ""
115
-
116
- logger.info("📋 기본 정보 추출 완료 - proc_inst_id: %s, tool: %s, tenant: %s",
117
- effective_proc_inst_id, tool_val, tenant_id)
118
112
 
119
113
  try:
120
- # 2단계: 컨텍스트 번들 조회
121
- logger.info("🔍 컨텍스트 번들 조회 중...")
122
- notify_emails, tenant_mcp, form_tuple, agents = await fetch_context_bundle(
123
- effective_proc_inst_id, tenant_id, tool_val, user_ids
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
124
123
  )
125
124
  form_id, form_fields, form_html = form_tuple
125
+ agents, users = users_group
126
+
127
+ logger.info("\n\n🔍 [데이터베이스 조회 결과]")
128
+ logger.info("-" * 60)
129
+
130
+ # Users 정보
131
+ if users:
132
+ user_info = []
133
+ for u in users[:5]:
134
+ name = u.get("name", u.get("user_name", "Unknown"))
135
+ email = u.get("email", "")
136
+ user_info.append(f"{name}({email})" if email else name)
137
+ logger.info("• Users (%d명): %s%s", len(users), ", ".join(user_info), "..." if len(users) > 5 else "")
138
+ else:
139
+ logger.info("• Users: 없음")
140
+
141
+ # Agents 정보
142
+ if agents:
143
+ agent_info = []
144
+ for a in agents[:5]:
145
+ name = a.get("name", a.get("agent_name", "Unknown"))
146
+ tools = a.get("tools", [])
147
+ tool_names = [t.get("name", str(t)) for t in tools[:3]] if tools else []
148
+ tool_str = f"[{', '.join(tool_names)}]" if tool_names else ""
149
+ agent_info.append(f"{name}{tool_str}")
150
+ logger.info("• Agents (%d개): %s%s", len(agents), ", ".join(agent_info), "..." if len(agents) > 5 else "")
151
+ else:
152
+ logger.info("• Agents: 없음")
153
+
154
+ # Form 정보
155
+ if form_fields:
156
+ pretty_json = json.dumps(form_fields, ensure_ascii=False, separators=(',', ':'))
157
+ logger.info("• Form: %s (%d개 필드) - %s", form_id, len(form_fields), pretty_json)
158
+ else:
159
+ logger.info("• Form: %s (필드 없음)", form_id)
126
160
 
127
- logger.info("📦 컨텍스트 번들 조회 완료 - agents: %d개, notify_emails: %s",
128
- len(agents) if isinstance(agents, list) else 0,
129
- "있음" if notify_emails else "없음")
161
+ # Notify 정보
162
+ if notify_emails:
163
+ email_list = notify_emails.split(',') if ',' in notify_emails else [notify_emails]
164
+ logger.info("• Notify (%d개): %s", len(email_list),
165
+ ", ".join(email_list[:3]) + ("..." if len(email_list) > 3 else ""))
166
+ else:
167
+ logger.info("• Notify: 없음")
168
+
169
+ # MCP 정보 - 상세 표시
170
+ if tenant_mcp:
171
+ logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재합니다.", tenant_id)
172
+ else:
173
+ logger.info("• %s 테넌트에 연결된 MCP 설정 정보가 존재하지 않습니다.", tenant_id)
174
+
175
+ # 피드백 처리
176
+ feedback_data = self.row.get("feedback")
177
+ content_data = self.row.get("output") or self.row.get("draft")
178
+ summarized_feedback = ""
179
+ if feedback_data:
180
+ logger.info("\n\n📝 [피드백 처리]")
181
+ logger.info("-" * 60)
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
+ self._extra_context = {
188
+ "id": self.row.get("id"),
189
+ "proc_inst_id": effective_proc_inst_id,
190
+ "root_proc_inst_id": self.row.get("root_proc_inst_id"),
191
+ "activity_name": self.row.get("activity_name"),
192
+ "agents": agents,
193
+ "users": users,
194
+ "tenant_mcp": tenant_mcp,
195
+ "form_fields": form_fields,
196
+ "form_html": form_html,
197
+ "form_id": form_id,
198
+ "notify_user_emails": notify_emails,
199
+ "summarized_feedback": summarized_feedback,
200
+ }
201
+
202
+ logger.info("\n\n🎉 [컨텍스트 준비 완료] 모든 데이터 준비됨")
203
+ logger.info("-"*60)
130
204
 
131
205
  except Exception as e:
132
- logger.error("❌ 컨텍스트 번들 조회 실패: %s", str(e))
133
- # 사용자 친화 요약은 상위 경계에서 한 번만 기록하도록 넘김
206
+ logger.error("❌ [데이터 조회 실패] %s", str(e))
134
207
  raise ContextPreparationError(e)
135
208
 
136
- # 3단계: 컨텍스트 구성
137
- logger.info("🏗️ 컨텍스트 구성 중...")
138
- self._extra_context = {
139
- "id": self.row.get("id"),
140
- "proc_inst_id": effective_proc_inst_id,
141
- "root_proc_inst_id": self.row.get("root_proc_inst_id"),
142
- "activity_name": self.row.get("activity_name"),
143
- "agents": agents,
144
- "tenant_mcp": tenant_mcp,
145
- "form_fields": form_fields,
146
- "form_html": form_html,
147
- "form_id": form_id,
148
- "notify_user_emails": notify_emails,
149
- }
150
-
151
- logger.info("✅ 컨텍스트 준비 완료! (agents=%d개)",
152
- len(agents) if isinstance(agents, list) else 0)
153
-
154
209
  def get_user_input(self) -> str:
155
210
  return self._user_input
156
211
 
@@ -336,11 +391,18 @@ class ProcessGPTAgentServer:
336
391
 
337
392
  while self.is_running and not self._shutdown_event.is_set():
338
393
  try:
339
- logger.info("🔍 Polling for tasks (agent_orch=%s)...", self.agent_orch)
394
+ logger.info("\n\n" + "-"*80)
395
+ logger.info("🔍 [폴링 시작] 작업 대기 중... (agent_orch=%s)", self.agent_orch)
396
+ logger.info("-"*80)
397
+
340
398
  row = await polling_pending_todos(self.agent_orch, get_consumer_id())
341
399
 
342
400
  if row:
343
- logger.info("✅ 새 작업: %s (proc=%s, activity=%s)", row.get("id"), row.get("proc_inst_id"), row.get("activity_name"))
401
+ logger.info("\n\n" + "-"*80)
402
+ logger.info("✅ [새 작업 발견] Task ID: %s", row.get("id"))
403
+ logger.info("• Activity: %s | Tool: %s | Tenant: %s",
404
+ row.get("activity_name"), row.get("tool"), row.get("tenant_id"))
405
+ logger.info("-"*80)
344
406
  try:
345
407
  self._current_todo_id = str(row.get("id"))
346
408
  await self.process_todolist_item(row)
@@ -380,29 +442,23 @@ class ProcessGPTAgentServer:
380
442
  4) 예외 재전달(상위 루프는 죽지 않고 다음 폴링)
381
443
  """
382
444
  task_id = row.get("id")
383
- logger.info("\n🎯 작업 처리 시작 - Task ID: %s", task_id)
384
- logger.info("📝 작업 정보 - proc_inst_id: %s, activity: %s, tool: %s",
385
- row.get("proc_inst_id"), row.get("activity_name"), row.get("tool"))
445
+ logger.info("\n🎯 [작업 처리 시작] Task ID: %s", task_id)
386
446
 
387
447
  friendly_text: Optional[str] = None
388
448
 
389
449
  try:
390
450
  # 1) 컨텍스트 준비 (실패 시 ContextPreparationError로 올라옴)
391
- logger.info("🔧 컨텍스트 준비 단계 시작...")
392
451
  context = ProcessGPTRequestContext(row)
393
452
  await context.prepare_context()
394
- logger.info("✅ 컨텍스트 준비 완료")
395
453
 
396
454
  # 2) 실행
397
- logger.info("🤖 에이전트 실행 단계 시작...")
455
+ logger.info("\n\n🤖 [Agent Orchestrator 실행]")
456
+ logger.info("-" * 60)
398
457
  event_queue = ProcessGPTEventQueue(str(task_id), self.agent_orch, row.get("proc_inst_id"))
399
458
  await self.agent_executor.execute(context, event_queue)
400
- logger.info("✅ 에이전트 실행 완료")
401
-
402
- # 3) 정상 완료 이벤트
403
- logger.info("🏁 작업 완료 처리 중...")
404
459
  event_queue.task_done()
405
- logger.info("🎉 작업 완료: %s\n", task_id)
460
+ logger.info("\n🎉 [Agent Orchestrator 완료] Task ID: %s", task_id)
461
+ logger.info("-"*60)
406
462
 
407
463
  except Exception as e:
408
464
  logger.error("❌ 작업 처리 중 오류 발생: %s", str(e))
@@ -0,0 +1,190 @@
1
+ import os
2
+ import logging
3
+ import traceback
4
+ from typing import Any, Dict, Optional, List
5
+ from typing import Iterable, Union
6
+ from openai import AsyncOpenAI
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ # ─────────────────────────────
13
+ # Lazy Singleton OpenAI Client
14
+ # ─────────────────────────────
15
+ _client: Optional["AsyncOpenAI"] = None # type: ignore[name-defined]
16
+
17
+ def _require_env(name: str, default: Optional[str] = None) -> str:
18
+ v = os.getenv(name, default if default is not None else "")
19
+ if not v:
20
+ raise RuntimeError(f"Missing required environment variable: {name}")
21
+ return v
22
+
23
+ def get_client() -> "AsyncOpenAI": # type: ignore[name-defined]
24
+ global _client
25
+ if _client is not None:
26
+ return _client
27
+ if AsyncOpenAI is None:
28
+ raise RuntimeError("OpenAI SDK (async) is not available")
29
+ base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
30
+ api_key = _require_env("OPENAI_API_KEY", "")
31
+ _client = AsyncOpenAI(base_url=base_url, api_key=api_key)
32
+ return _client
33
+
34
+ # ─────────────────────────────
35
+ # 공통 LLM 호출 유틸
36
+ # ─────────────────────────────
37
+ async def _llm_request(system: str, user: str, model_env: str, default_model: str) -> str:
38
+ model_name = os.getenv(model_env, default_model)
39
+ logger.info("📡 LLM 요청 전송 (모델: %s)", model_name)
40
+
41
+ client = get_client()
42
+ # responses API (신규)
43
+ resp = await client.responses.create(
44
+ model=model_name,
45
+ input=[
46
+ {"role": "system", "content": system},
47
+ {"role": "user", "content": user},
48
+ ],
49
+ )
50
+
51
+ # 다양한 SDK 출력 구조 호환
52
+ text: Optional[str] = None
53
+ try:
54
+ text = getattr(resp, "output_text", None) # 최신 필드
55
+ except Exception:
56
+ text = None
57
+
58
+ if not text and hasattr(resp, "choices") and resp.choices: # 구 구조 호환
59
+ choice0 = resp.choices[0]
60
+ text = getattr(getattr(choice0, "message", None), "content", None)
61
+
62
+ if not text:
63
+ raise RuntimeError("No text in LLM response")
64
+
65
+ return text.strip()
66
+
67
+ # ─────────────────────────────
68
+ # 공개 API
69
+ # ─────────────────────────────
70
+ async def summarize_error_to_user(exc: Exception, meta: Dict[str, Any]) -> str:
71
+ """
72
+ 예외 정보를 바탕으로 사용자 친화적인 5줄 요약을 생성.
73
+ - 모델: gpt-4.1-nano (환경변수 ERROR_SUMMARY_MODEL로 재정의 가능)
74
+ - 폴백: 없음 (LLM 실패 시 예외를 상위로 전파)
75
+ """
76
+ logger.info("🔍 오류 컨텍스트 분석 시작")
77
+
78
+ err_text = f"{type(exc).__name__}: {str(exc)}"
79
+
80
+ # 가벼운 스택 문자열 (상위 3프레임)
81
+ try:
82
+ tb = "".join(traceback.TracebackException.from_exception(exc, limit=3).format())
83
+ except Exception:
84
+ tb = traceback.format_exc(limit=3)
85
+
86
+ meta_items: List[str] = []
87
+ for k in ("task_id", "proc_inst_id", "agent_orch", "tool"):
88
+ v = meta.get(k)
89
+ if v:
90
+ meta_items.append(f"{k}={v}")
91
+ meta_text = ", ".join(meta_items)
92
+
93
+ logger.info("📋 오류 컨텍스트 정리 완료 - %s", meta_text)
94
+
95
+ system = (
96
+ "당신은 엔터프라이즈 SDK의 오류 비서입니다. "
97
+ "사용자(비개발자도 이해 가능)를 위해, 아래 조건을 정확히 지켜 5줄로 한국어 설명을 만드세요.\n"
98
+ "형식: 각 줄은 1문장씩, 총 5줄.\n"
99
+ "포함 요소: ①무슨 문제인지(원인 추정) ②어떤 영향이 있는지 ③즉시 할 일(대처) "
100
+ "④재발 방지 팁 ⑤필요시 지원 요청 경로.\n"
101
+ "과장 금지, 간결하고 친절하게."
102
+ )
103
+ user = (
104
+ f"[오류요약대상]\n"
105
+ f"- 컨텍스트: {meta_text}\n"
106
+ f"- 에러: {err_text}\n"
107
+ f"- 스택(상위 3프레임):\n{tb}\n"
108
+ f"위 정보를 바탕으로 5줄 설명을 출력하세요."
109
+ )
110
+
111
+ try:
112
+ text = await _llm_request(system, user, "ERROR_SUMMARY_MODEL", "gpt-4.1-nano")
113
+ logger.info("✅ LLM 오류 요약 생성 완료")
114
+ return text
115
+ except Exception as e:
116
+ logger.warning("⚠️ LLM 오류 요약 생성 실패: %s", e, exc_info=True)
117
+ # 폴백 없이 상위 전파
118
+ raise
119
+
120
+ async def summarize_feedback(feedback_data: List[dict], content_data: dict = {}) -> str:
121
+ """
122
+ 피드백과 결과물을 바탕으로 통합된 피드백 요약을 생성.
123
+ - 모델: gpt-4.1-nano (환경변수 FEEDBACK_SUMMARY_MODEL로 재정의 가능)
124
+ - 폴백: 없음 (LLM 실패 시 예외를 상위로 전파)
125
+ """
126
+ logger.info(
127
+ "🔍 피드백 요약 처리 시작 | 피드백: %s, 결과물: %s자",
128
+ feedback_data, content_data)
129
+
130
+ system_prompt = _get_feedback_system_prompt()
131
+ user_prompt = _create_feedback_summary_prompt(feedback_data, content_data)
132
+
133
+ try:
134
+ text = await _llm_request(system_prompt, user_prompt, "FEEDBACK_SUMMARY_MODEL", "gpt-4.1-nano")
135
+ logger.info("✅ LLM 피드백 요약 생성 완료")
136
+ return text
137
+ except Exception as e:
138
+ logger.error("❌ LLM 피드백 요약 생성 실패: %s", e, exc_info=True)
139
+ # 폴백 없이 상위 전파
140
+ raise
141
+
142
+ # ─────────────────────────────
143
+ # 프롬프트 유틸
144
+ # ─────────────────────────────
145
+ def _create_feedback_summary_prompt(feedback_data: List[dict], content_data: dict = {}) -> str:
146
+ """피드백 정리 프롬프트 - 현재 결과물과 피드백을 함께 분석"""
147
+ blocks: List[str] = ["다음은 사용자의 피드백과 결과물입니다. 이를 분석하여 통합된 피드백을 작성해주세요:"]
148
+ if feedback_data:
149
+ blocks.append(f"=== 피드백 내용 ===\n{feedback_data}")
150
+ if content_data:
151
+ blocks.append(f"=== 현재 결과물/작업 내용 ===\n{content_data}")
152
+
153
+ blocks.append(
154
+ """**상황 분석 및 처리 방식:**
155
+ - **현재 결과물을 보고 어떤 점이 문제인지, 개선이 필요한지 판단**
156
+ - 피드백이 있다면 그 의도와 요구사항을 정확히 파악
157
+ - 결과물 자체가 마음에 안들어서 다시 작업을 요청하는 경우일 수 있음
158
+ - 작업 방식이나 접근법이 잘못되었다고 판단하는 경우일 수 있음
159
+ - 부분적으로는 좋지만 특정 부분의 수정이나 보완이 필요한 경우일 수 있음
160
+ - 현재 결과물에 매몰되지 말고, 실제 어떤 부분이 문제인지 파악하여 개선 방안을 제시
161
+
162
+ **피드백 통합 원칙:**
163
+ - **가장 최신 피드백을 최우선으로 반영**
164
+ - 결과물과 피드백을 종합적으로 분석하여 핵심 문제점 파악
165
+ - **시간 흐름을 파악하여 피드백들 간의 연결고리와 문맥을 이해**
166
+ - 구체적이고 실행 가능한 개선사항 제시
167
+ - **자연스럽고 통합된 하나의 완전한 피드백으로 작성**
168
+ - 최대 1000자까지 허용하여 상세히 작성
169
+
170
+ **중요한 상황별 처리:**
171
+ - 결과물 품질에 대한 불만 → **품질 개선** 요구
172
+ - 작업 방식에 대한 불만 → **접근법 변경** 요구
173
+ - 이전에 저장을 했는데 잘못 저장되었다면 → **수정**이 필요
174
+ - 이전에 조회만 했는데 저장이 필요하다면 → **저장**이 필요
175
+ - 부분적 수정이 필요하다면 → **특정 부분 개선** 요구
176
+
177
+ 출력 형식: 현재 상황을 종합적으로 분석한 완전한 피드백 문장 (다음 작업자가 즉시 이해하고 실행할 수 있도록)"""
178
+ )
179
+ return "\n\n".join(blocks)
180
+
181
+ def _get_feedback_system_prompt() -> str:
182
+ """피드백 요약용 시스템 프롬프트"""
183
+ return """당신은 피드백 정리 전문가입니다.
184
+
185
+ 핵심 원칙:
186
+ - 최신 피드백을 최우선으로 하여 시간 흐름을 파악
187
+ - 피드백 간 문맥과 연결고리를 파악하여 하나의 완전한 요청으로 통합
188
+ - 자연스럽고 통합된 피드백으로 작성
189
+ - 구체적인 요구사항과 개선사항을 누락 없이 포함
190
+ - 다음 작업자가 즉시 이해할 수 있도록 명확하게"""
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "process-gpt-agent-sdk"
7
- version = "0.3.17"
7
+ version = "0.3.19"
8
8
  description = "Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,25 +0,0 @@
1
- from .processgpt_agent_framework import ProcessGPTAgentServer
2
- from .database import (
3
- initialize_db,
4
- get_consumer_id,
5
- polling_pending_todos,
6
- record_event,
7
- record_events_bulk,
8
- save_task_result,
9
- update_task_error,
10
- fetch_context_bundle,
11
- )
12
- from .utils import summarize_error_to_user
13
-
14
- __all__ = [
15
- "ProcessGPTAgentServer",
16
- "initialize_db",
17
- "get_consumer_id",
18
- "polling_pending_todos",
19
- "record_event",
20
- "record_events_bulk",
21
- "save_task_result",
22
- "update_task_error",
23
- "fetch_context_bundle",
24
- "summarize_error_to_user",
25
- ]
@@ -1,100 +0,0 @@
1
- import os
2
- import traceback
3
- import logging
4
- from typing import Any, Dict
5
-
6
- # OpenAI 호환 엔드포인트 사용 (환경변수 기반)
7
- # OPENAI_API_KEY, OPENAI_BASE_URL(required if not default)
8
- try:
9
- from openai import OpenAI
10
- except Exception: # 라이브러리 미설치/호환 환경 대비
11
- OpenAI = None # type: ignore
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- async def summarize_error_to_user(exc: Exception, meta: Dict[str, Any]) -> str:
17
- """
18
- 예외 정보를 바탕으로 사용자 친화적인 5줄 요약을 생성.
19
- - 모델: gpt-4.1-nano (요청사항 반영)
20
- - 실패 시 Fallback: 간단한 수동 요약문
21
- """
22
- # 오류 컨텍스트 정리
23
- logger.info("🔍 오류 컨텍스트 분석 중...")
24
- err_text = f"{type(exc).__name__}: {str(exc)}"
25
- tb = traceback.format_exc(limit=3)
26
- meta_lines = [
27
- f"task_id={meta.get('task_id')}",
28
- f"proc_inst_id={meta.get('proc_inst_id')}",
29
- f"agent_orch={meta.get('agent_orch')}",
30
- f"tool={meta.get('tool')}",
31
- ]
32
- meta_text = ", ".join([x for x in meta_lines if x])
33
- logger.info("📋 오류 컨텍스트 분석 완료 - %s", meta_text)
34
-
35
- system = (
36
- "당신은 엔터프라이즈 SDK의 오류 비서입니다. "
37
- "사용자(비개발자도 이해 가능)를 위해, 아래 조건을 정확히 지켜 5줄로 한국어 설명을 만드세요.\n"
38
- "형식: 각 줄은 1문장씩, 총 5줄.\n"
39
- "포함 요소: ①무슨 문제인지(원인 추정) ②어떤 영향이 있는지 ③즉시 할 일(대처) "
40
- "④재발 방지 팁 ⑤필요시 지원 요청 경로.\n"
41
- "과장 금지, 간결하고 친절하게."
42
- )
43
- user = (
44
- f"[오류요약대상]\n"
45
- f"- 컨텍스트: {meta_text}\n"
46
- f"- 에러: {err_text}\n"
47
- f"- 스택(상위 3프레임):\n{tb}\n"
48
- f"위 정보를 바탕으로 5줄 설명을 출력하세요."
49
- )
50
-
51
- try:
52
- if OpenAI is None:
53
- logger.warning("⚠️ OpenAI SDK 사용 불가 - Fallback 모드로 전환")
54
- raise RuntimeError("OpenAI SDK not available")
55
-
56
- logger.info("🤖 OpenAI 클라이언트 초기화 중...")
57
- client = OpenAI(
58
- base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
59
- api_key=os.getenv("OPENAI_API_KEY", ""),
60
- )
61
-
62
- model_name = os.getenv("ERROR_SUMMARY_MODEL", "gpt-4.1-nano")
63
- logger.info("📡 LLM 요청 전송 중... (모델: %s)", model_name)
64
-
65
- # responses API (신규 SDK)
66
- resp = client.responses.create(
67
- model=model_name,
68
- input=[{"role": "system", "content": system},
69
- {"role": "user", "content": user}],
70
- )
71
-
72
- logger.info("🔍 LLM 응답 분석 중...")
73
- # 텍스트 추출(호환성 고려)
74
- text = None
75
- try:
76
- text = resp.output_text # type: ignore[attr-defined]
77
- except Exception:
78
- # 다른 필드 구조 호환
79
- if hasattr(resp, "choices") and resp.choices:
80
- text = getattr(resp.choices[0].message, "content", None) # type: ignore
81
- if not text:
82
- raise RuntimeError("No text in LLM response")
83
-
84
- logger.info("✅ LLM 오류 요약 생성 완료")
85
- return text.strip()
86
-
87
- except Exception as e:
88
- logger.warning("⚠️ LLM 오류 요약 생성 실패: %s - Fallback 모드로 전환", str(e))
89
- # Fallback: 간단 5줄
90
- logger.info("📝 Fallback 오류 요약 생성 중...")
91
-
92
- fallback_text = (
93
- "1) 처리 중 알 수 없는 오류가 발생했어요(환경/입력 값 문제일 수 있어요).\n"
94
- "2) 작업 결과가 저장되지 않았거나 일부만 반영됐을 수 있어요.\n"
95
- "3) 입력 값과 네트워크 상태를 확인하고, 다시 시도해 주세요.\n"
96
- "4) 같은 문제가 반복되면 로그와 설정(키/URL/권한)을 점검해 주세요.\n"
97
- "5) 계속되면 관리자나 운영팀에 문의해 원인 분석을 요청해 주세요."
98
- )
99
- logger.info("✅ Fallback 오류 요약 생성 완료")
100
- return fallback_text