process-gpt-agent-sdk 0.3.15__tar.gz → 0.3.17__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.15
3
+ Version: 0.3.17
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
@@ -13,14 +13,10 @@ Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: supabase>=2.0.0
16
+ Requires-Dist: openai>=1.30.0
16
17
  Requires-Dist: python-dotenv>=1.0.0
17
- Requires-Dist: click>=8.0.0
18
- Requires-Dist: asyncio-mqtt>=0.13.0
19
- Requires-Dist: jsonschema>=4.0.0
20
- Requires-Dist: structlog>=23.0.0
21
- Requires-Dist: typing-extensions>=4.0.0
22
- Requires-Dist: python-dateutil>=2.8.0
23
18
  Requires-Dist: a2a-sdk==0.3.0
19
+ Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
24
20
 
25
21
  # ProcessGPT Agent Framework
26
22
  ## A2A SDK 연동을 위한 경량 에이전트 서버 프레임워크
@@ -40,7 +36,6 @@ Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이
40
36
  |------------|------|-----------|----------------|
41
37
  | `task_started` | 작업 시작 | 작업 처리 시작시 | 수동 설정 |
42
38
  | `task_completed` | 작업 완료 | 작업 정상 완료시 | 수동 설정 |
43
- | `crew_completed` | 크루 작업 완료 | 서버가 작업 종료시 | **자동 설정** |
44
39
  | `tool_usage_started` | 도구 사용 시작 | 외부 도구/API 호출 시작 | 수동 설정 |
45
40
  | `tool_usage_finished` | 도구 사용 완료 | 외부 도구/API 호출 완료 | 수동 설정 |
46
41
  | `human_asked` | 사용자 입력 요청 | HITL 패턴 사용시 | **자동 설정** |
@@ -662,3 +657,7 @@ python my_server.py
662
657
  - **TaskStatusUpdateEvent** → `events` 테이블 (`data` 컬럼)
663
658
  - **TaskArtifactUpdateEvent** → `todolist` 테이블 (`output` 컬럼)
664
659
  - 래퍼 자동 제거 후 순수 payload만 저장
660
+
661
+
662
+ ## 버전업
663
+ ./release.sh 버전
@@ -16,7 +16,6 @@ Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이
16
16
  |------------|------|-----------|----------------|
17
17
  | `task_started` | 작업 시작 | 작업 처리 시작시 | 수동 설정 |
18
18
  | `task_completed` | 작업 완료 | 작업 정상 완료시 | 수동 설정 |
19
- | `crew_completed` | 크루 작업 완료 | 서버가 작업 종료시 | **자동 설정** |
20
19
  | `tool_usage_started` | 도구 사용 시작 | 외부 도구/API 호출 시작 | 수동 설정 |
21
20
  | `tool_usage_finished` | 도구 사용 완료 | 외부 도구/API 호출 완료 | 수동 설정 |
22
21
  | `human_asked` | 사용자 입력 요청 | HITL 패턴 사용시 | **자동 설정** |
@@ -638,3 +637,7 @@ python my_server.py
638
637
  - **TaskStatusUpdateEvent** → `events` 테이블 (`data` 컬럼)
639
638
  - **TaskArtifactUpdateEvent** → `todolist` 테이블 (`output` 컬럼)
640
639
  - 래퍼 자동 제거 후 순수 payload만 저장
640
+
641
+
642
+ ## 버전업
643
+ ./release.sh 버전
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-gpt-agent-sdk
3
- Version: 0.3.15
3
+ Version: 0.3.17
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
@@ -13,14 +13,10 @@ Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: supabase>=2.0.0
16
+ Requires-Dist: openai>=1.30.0
16
17
  Requires-Dist: python-dotenv>=1.0.0
17
- Requires-Dist: click>=8.0.0
18
- Requires-Dist: asyncio-mqtt>=0.13.0
19
- Requires-Dist: jsonschema>=4.0.0
20
- Requires-Dist: structlog>=23.0.0
21
- Requires-Dist: typing-extensions>=4.0.0
22
- Requires-Dist: python-dateutil>=2.8.0
23
18
  Requires-Dist: a2a-sdk==0.3.0
19
+ Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
24
20
 
25
21
  # ProcessGPT Agent Framework
26
22
  ## A2A SDK 연동을 위한 경량 에이전트 서버 프레임워크
@@ -40,7 +36,6 @@ Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이
40
36
  |------------|------|-----------|----------------|
41
37
  | `task_started` | 작업 시작 | 작업 처리 시작시 | 수동 설정 |
42
38
  | `task_completed` | 작업 완료 | 작업 정상 완료시 | 수동 설정 |
43
- | `crew_completed` | 크루 작업 완료 | 서버가 작업 종료시 | **자동 설정** |
44
39
  | `tool_usage_started` | 도구 사용 시작 | 외부 도구/API 호출 시작 | 수동 설정 |
45
40
  | `tool_usage_finished` | 도구 사용 완료 | 외부 도구/API 호출 완료 | 수동 설정 |
46
41
  | `human_asked` | 사용자 입력 요청 | HITL 패턴 사용시 | **자동 설정** |
@@ -662,3 +657,7 @@ python my_server.py
662
657
  - **TaskStatusUpdateEvent** → `events` 테이블 (`data` 컬럼)
663
658
  - **TaskArtifactUpdateEvent** → `todolist` 테이블 (`output` 컬럼)
664
659
  - 래퍼 자동 제거 후 순수 payload만 저장
660
+
661
+
662
+ ## 버전업
663
+ ./release.sh 버전
@@ -7,4 +7,5 @@ process_gpt_agent_sdk.egg-info/requires.txt
7
7
  process_gpt_agent_sdk.egg-info/top_level.txt
8
8
  processgpt_agent_sdk/__init__.py
9
9
  processgpt_agent_sdk/database.py
10
- processgpt_agent_sdk/processgpt_agent_framework.py
10
+ processgpt_agent_sdk/processgpt_agent_framework.py
11
+ processgpt_agent_sdk/utils.py
@@ -0,0 +1,7 @@
1
+ supabase>=2.0.0
2
+ openai>=1.30.0
3
+ python-dotenv>=1.0.0
4
+ a2a-sdk==0.3.0
5
+
6
+ [:python_version < "3.11"]
7
+ typing-extensions>=4.0.0
@@ -1,33 +1,25 @@
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
- save_task_result,
8
- update_task_error,
9
- fetch_agent_data,
10
- fetch_all_agents,
11
- fetch_form_types,
12
- fetch_tenant_mcp_config,
13
- fetch_human_users_by_proc_inst_id,
14
- )
15
-
16
- __all__ = [
17
- "ProcessGPTAgentServer",
18
- "initialize_db",
19
- "get_consumer_id",
20
- "polling_pending_todos",
21
- "record_event",
22
- "save_task_result",
23
- "update_task_error",
24
- "fetch_agent_data",
25
- "fetch_all_agents",
26
- "fetch_form_types",
27
- "fetch_tenant_mcp_config",
28
- "fetch_human_users_by_proc_inst_id",
29
- ]
30
-
31
- __version__ = "0.3.12"
32
-
33
-
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
+ ]
@@ -0,0 +1,259 @@
1
+ import os
2
+ import json
3
+ import asyncio
4
+ import socket
5
+ from typing import Any, Dict, List, Optional, Tuple, Callable, TypeVar
6
+
7
+ from dotenv import load_dotenv
8
+ from supabase import Client, create_client
9
+ import logging
10
+ import random
11
+
12
+ T = TypeVar("T")
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # ------------------------------ Retry & JSON utils ------------------------------
16
+ async def _async_retry(
17
+ fn: Callable[[], Any],
18
+ *,
19
+ name: str,
20
+ retries: int = 3,
21
+ base_delay: float = 0.8,
22
+ fallback: Optional[Callable[[], Any]] = None,
23
+ ) -> Optional[Any]:
24
+ """
25
+ - 각 시도 실패: warning 로깅(시도/지연/에러 포함)
26
+ - 최종 실패: FATAL 로깅(스택 포함), 예외는 재전파하지 않고 None 반환(기존 정책 유지)
27
+ - fallback 이 있으면 실행(실패 시에도 로깅 후 None)
28
+ """
29
+ last_err: Optional[Exception] = None
30
+ for attempt in range(1, retries + 1):
31
+ try:
32
+ return await asyncio.to_thread(fn)
33
+ except Exception as e:
34
+ last_err = e
35
+ jitter = random.uniform(0, 0.3)
36
+ delay = base_delay * (2 ** (attempt - 1)) + jitter
37
+ logger.warning(
38
+ "retry warn: name=%s attempt=%d/%d delay=%.2fs error=%s",
39
+ name, attempt, retries, delay, str(e),
40
+ exc_info=e
41
+ )
42
+ await asyncio.sleep(delay)
43
+
44
+ # 최종 실패
45
+ if last_err is not None:
46
+ logger.error(
47
+ "FATAL: retry failed: name=%s retries=%s error=%s",
48
+ name, retries, str(last_err), exc_info=last_err
49
+ )
50
+
51
+ if fallback is not None:
52
+ try:
53
+ return fallback()
54
+ except Exception as fb_err:
55
+ logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
56
+ return None
57
+ return None
58
+
59
+ def _to_jsonable(value: Any) -> Any:
60
+ try:
61
+ if value is None or isinstance(value, (str, int, float, bool)):
62
+ return value
63
+ if isinstance(value, dict):
64
+ return {str(k): _to_jsonable(v) for k, v in value.items()}
65
+ if isinstance(value, (list, tuple, set)):
66
+ return [_to_jsonable(v) for v in list(value)]
67
+ if hasattr(value, "__dict__"):
68
+ return _to_jsonable(vars(value))
69
+ return repr(value)
70
+ except Exception:
71
+ return repr(value)
72
+
73
+ # ------------------------------ DB Client ------------------------------
74
+ _supabase_client: Optional[Client] = None
75
+
76
+ def initialize_db() -> None:
77
+ global _supabase_client
78
+ if _supabase_client is not None:
79
+ return
80
+ try:
81
+ if os.getenv("ENV") != "production":
82
+ load_dotenv()
83
+ supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
84
+ supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
85
+ if not supabase_url or not supabase_key:
86
+ raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
87
+ _supabase_client = create_client(supabase_url, supabase_key)
88
+ except Exception as e:
89
+ logger.error("initialize_db failed: %s", str(e), exc_info=e)
90
+ raise
91
+
92
+ def get_db_client() -> Client:
93
+ if _supabase_client is None:
94
+ raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
95
+ return _supabase_client
96
+
97
+ def get_consumer_id() -> str:
98
+ env_consumer = os.getenv("CONSUMER_ID")
99
+ if env_consumer:
100
+ return env_consumer
101
+ host = socket.gethostname()
102
+ pid = os.getpid()
103
+ return f"{host}:{pid}"
104
+
105
+ # ------------------------------ Polling ------------------------------
106
+ async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
107
+ """단일 RPC(fetch_pending_task) 호출: p_env 로 dev/prod 분기"""
108
+ if agent_orch is None:
109
+ agent_orch = ""
110
+ if consumer is None:
111
+ consumer = ""
112
+
113
+ def _call():
114
+ client = get_db_client()
115
+ consumer_id = consumer or socket.gethostname()
116
+
117
+ # ENV 값을 dev / (그외=prod) 로만 정규화
118
+ p_env = (os.getenv("ENV") or "").lower()
119
+ if p_env != "dev":
120
+ p_env = "prod"
121
+
122
+ resp = client.rpc(
123
+ "fetch_pending_task",
124
+ {
125
+ "p_agent_orch": agent_orch,
126
+ "p_consumer": consumer_id,
127
+ "p_limit": 1,
128
+ "p_env": p_env,
129
+ },
130
+ ).execute()
131
+
132
+ rows = resp.data or []
133
+ return rows[0] if rows else None
134
+
135
+ return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
136
+
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
+
171
+ # ------------------------------ Events & Results ------------------------------
172
+ async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
173
+ """
174
+ 이벤트 다건 저장:
175
+ - 성공: 'record_events_bulk ok'
176
+ - 실패(최종): '❌ record_events_bulk failed' (개수 포함)
177
+ """
178
+ if not payloads:
179
+ return
180
+
181
+ safe_list: List[Dict[str, Any]] = []
182
+ for p in payloads:
183
+ sp = _to_jsonable(p)
184
+ if isinstance(sp, dict) and sp.get("status", "") == "":
185
+ sp["status"] = None
186
+ safe_list.append(sp)
187
+
188
+ def _call():
189
+ client = get_db_client()
190
+ return client.rpc("record_events_bulk", {"p_events": safe_list}).execute()
191
+
192
+ res = await _async_retry(_call, name="record_events_bulk", fallback=lambda: None)
193
+ if res is None:
194
+ logger.error("❌ record_events_bulk failed: events not persisted count=%d", len(safe_list))
195
+ else:
196
+ logger.info("record_events_bulk ok: count=%d", len(safe_list))
197
+
198
+ async def record_event(payload: Dict[str, Any]) -> None:
199
+ """
200
+ 단건 이벤트 저장.
201
+ - 성공: 'record_event ok'
202
+ - 실패(최종): '❌ record_event failed'
203
+ - 실패해도 워크플로우는 계속(요청사항)
204
+ """
205
+ if not payload:
206
+ return
207
+
208
+ def _call():
209
+ client = get_db_client()
210
+ safe_payload = _to_jsonable(payload)
211
+ if isinstance(safe_payload, dict) and safe_payload.get("status", "") == "":
212
+ safe_payload["status"] = None
213
+ return client.table("events").insert(safe_payload).execute()
214
+
215
+ res = await _async_retry(_call, name="record_event", fallback=lambda: None)
216
+ if res is None:
217
+ logger.error("❌ record_event failed =%s", payload.get("event_type"))
218
+ else:
219
+ logger.info("record_event ok: event_type=%s", payload.get("event_type"))
220
+
221
+ async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
222
+ if not todo_id:
223
+ logger.error("save_task_result invalid todo_id: %s", str(todo_id))
224
+ return
225
+
226
+ def _safe(val: Any) -> Any:
227
+ try:
228
+ return _to_jsonable(val)
229
+ except Exception:
230
+ try:
231
+ return {"repr": repr(val)}
232
+ except Exception:
233
+ return {"error": "unserializable payload"}
234
+
235
+ def _call():
236
+ client = get_db_client()
237
+ payload = _safe(result)
238
+ return client.rpc("save_task_result", {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)}).execute()
239
+
240
+ res = await _async_retry(_call, name="save_task_result", fallback=lambda: None)
241
+ if res is None:
242
+ logger.error("❌ save_task_result failed todo_id=%s", todo_id)
243
+ else:
244
+ logger.info("save_task_result ok todo_id=%s", todo_id)
245
+
246
+ # ------------------------------ Failure Status ------------------------------
247
+ async def update_task_error(todo_id: str) -> None:
248
+ if not todo_id:
249
+ return
250
+
251
+ def _call():
252
+ client = get_db_client()
253
+ return client.table("todolist").update({"draft_status": "FAILED", "consumer": None}).eq("id", todo_id).execute()
254
+
255
+ res = await _async_retry(_call, name="update_task_error", fallback=lambda: None)
256
+ if res is None:
257
+ logger.error("❌ update_task_error failed todo_id=%s", todo_id)
258
+ else:
259
+ logger.info("update_task_error ok todo_id=%s", todo_id)