process-gpt-agent-sdk 0.1.2__py3-none-any.whl → 0.1.4__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-gpt-agent-sdk
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -27,8 +27,9 @@ Requires-Dist: anyio>=4.4.0
27
27
  Requires-Dist: pydantic>=2.7.0
28
28
  Requires-Dist: crewai>=0.51.0
29
29
  Requires-Dist: crewai-tools>=0.8.2
30
- Requires-Dist: mem0>=0.1.0
31
30
  Requires-Dist: mcp>=1.0.0
31
+ Provides-Extra: mem0
32
+ Requires-Dist: mem0>=0.1.0; extra == "mem0"
32
33
 
33
34
  # ProcessGPT Agent Framework
34
35
 
@@ -0,0 +1,17 @@
1
+ processgpt_agent_sdk/__init__.py,sha256=IvAL5WBZhI83LYQogRP6-i04bxZkhmkgmES4FRQY888,185
2
+ processgpt_agent_sdk/server.py,sha256=cMta6gk0NVxJSN778skGaT0cCTlpK7UKiMzER2UTRcU,8115
3
+ processgpt_agent_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ processgpt_agent_sdk/core/database.py,sha256=a87Y1DLxMIm3Yu_-6LWWANnZKJgU265Nn7cKjOB1s8Y,13667
5
+ processgpt_agent_sdk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ processgpt_agent_sdk/tools/knowledge_tools.py,sha256=-IPQB6A59wMTBsZUDM-7q10IHMz30X-m_vp6RgB6yHA,8995
7
+ processgpt_agent_sdk/tools/safe_tool_loader.py,sha256=a2Twnldu4VliHk41OY03lxqM91cK6-MRmjXuSHbcKqk,5647
8
+ processgpt_agent_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ processgpt_agent_sdk/utils/context_manager.py,sha256=Vqx3PUSNR_V3t7CbK6_W0xU5eb2IJPpn-YFLlfncIL4,1174
10
+ processgpt_agent_sdk/utils/crewai_event_listener.py,sha256=-YIJCvrTCsS1Tz0JBqgXRcj2ziv3Pqkak_iez-pQUBI,8686
11
+ processgpt_agent_sdk/utils/event_handler.py,sha256=Kga4uc4qSTywkFA0dJy699wcbogVkjbx90-psLXNnv0,1718
12
+ processgpt_agent_sdk/utils/logger.py,sha256=JmA5_A1DO3oRqZpiT4ZpNrPjYGWRAR38V-aBNr4-R1c,878
13
+ processgpt_agent_sdk/utils/summarizer.py,sha256=XV7e4pyKEK7NiGK_PAc1xorKSO2MKlSHCAiV7slOdus,4527
14
+ process_gpt_agent_sdk-0.1.4.dist-info/METADATA,sha256=9gqChy6DknuWhMSliDzN1_n5usQFKGpUzZ0a8ezOOP8,12935
15
+ process_gpt_agent_sdk-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ process_gpt_agent_sdk-0.1.4.dist-info/top_level.txt,sha256=Xe6zrj3_3Vv7d0pl5RRtenVUckwOVBVLQn2P03j5REo,21
17
+ process_gpt_agent_sdk-0.1.4.dist-info/RECORD,,
@@ -1,7 +1,7 @@
1
- from .server import ProcessGPTAgentServer, ProcessGPTRequestContext, ProcessGPTEventQueue
2
-
3
- __all__ = [
4
- "ProcessGPTAgentServer",
5
- "ProcessGPTRequestContext",
6
- "ProcessGPTEventQueue",
7
- ]
1
+ from .server import ProcessGPTAgentServer, ProcessGPTRequestContext, ProcessGPTEventQueue
2
+
3
+ __all__ = [
4
+ "ProcessGPTAgentServer",
5
+ "ProcessGPTRequestContext",
6
+ "ProcessGPTEventQueue",
7
+ ]
@@ -1,8 +1,22 @@
1
+ """ProcessGPT DB utilities
2
+
3
+ 역할:
4
+ - Supabase 연결/초기화
5
+ - 안전한 RPC/CRUD 호출(재시도/폴백 포함)
6
+ - 이벤트 기록, 작업 클레임/상태/저장/조회, 사용자·에이전트·폼·테넌트 조회
7
+
8
+ 반환 규칙(폴백 포함):
9
+ - Optional 단건 조회 계열 → 실패 시 None
10
+ - 목록/시퀀스 계열 → 실패 시 빈 리스트 []
11
+ - 변경/기록 계열 → 실패 시 경고 로그만 남기고 None
12
+ """
13
+
1
14
  import os
2
15
  import json
3
16
  import asyncio
4
17
  import socket
5
18
  import uuid
19
+ from datetime import datetime
6
20
  from typing import Any, Dict, List, Optional, Tuple
7
21
 
8
22
  from dotenv import load_dotenv
@@ -17,273 +31,343 @@ from ..utils.logger import handle_error as _emit_error, log as _emit_log
17
31
 
18
32
 
19
33
  async def _async_retry(
20
- fn: Callable[[], T],
21
- *,
22
- name: str,
23
- retries: int = 3,
24
- base_delay: float = 0.8,
25
- fallback: Optional[Callable[[], T]] = None,
34
+ fn: Callable[[], T],
35
+ *,
36
+ name: str,
37
+ retries: int = 3,
38
+ base_delay: float = 0.8,
39
+ fallback: Optional[Callable[[], T]] = None,
26
40
  ) -> Optional[T]:
27
- """재시도 유틸(지수 백오프+jitter)."""
28
- last_err: Optional[Exception] = None
29
- for attempt in range(1, retries + 1):
30
- try:
31
- return await asyncio.to_thread(fn)
32
- except Exception as e:
33
- last_err = e
34
- jitter = random.uniform(0, 0.3)
35
- delay = base_delay * (2 ** (attempt - 1)) + jitter
36
- _emit_log(f"{name} 재시도 {attempt}/{retries} (delay={delay:.2f}s): {e}", level=logging.WARNING)
37
- await asyncio.sleep(delay)
38
- _emit_log(f"{name} 최종 실패: {last_err}", level=logging.ERROR)
39
- if fallback is not None:
40
- try:
41
- fb_val = fallback()
42
- _emit_log(f"{name} 폴백 사용", level=logging.WARNING)
43
- return fb_val
44
- except Exception as e:
45
- _emit_log(f"{name} 폴백 실패: {e}", level=logging.ERROR)
46
- return None
47
-
48
-
41
+ """재시도 유틸(지수 백오프+jitter).
42
+
43
+ - 최종 실패 시: fallback이 있으면 실행, 없으면 None
44
+ - 로그: 시도/지연/최종 실패/폴백 사용/폴백 실패
45
+ """
46
+ last_err: Optional[Exception] = None
47
+ for attempt in range(1, retries + 1):
48
+ try:
49
+ # 블로킹 DB 호출은 스레드로 위임해 이벤트 루프 차단 방지
50
+ return await asyncio.to_thread(fn)
51
+ except Exception as e:
52
+ last_err = e
53
+ jitter = random.uniform(0, 0.3)
54
+ delay = base_delay * (2 ** (attempt - 1)) + jitter
55
+ _emit_log(f"{name} 재시도 {attempt}/{retries} (delay={delay:.2f}s): {e}", level=logging.WARNING)
56
+ await asyncio.sleep(delay)
57
+ _emit_log(f"{name} 최종 실패: {last_err}", level=logging.ERROR)
58
+ if fallback is not None:
59
+ try:
60
+ fb_val = fallback()
61
+ _emit_log(f"{name} 폴백 사용", level=logging.WARNING)
62
+ return fb_val
63
+ except Exception as e:
64
+ _emit_log(f"{name} 폴백 실패: {e}", level=logging.ERROR)
65
+ return None
66
+
67
+
68
+
69
+ # ------------------------------
70
+ # Consumer 식별자 도우미
71
+ # ------------------------------
49
72
  _supabase_client: Optional[Client] = None
50
73
 
51
74
 
52
75
  def initialize_db() -> None:
53
- global _supabase_client
54
- if _supabase_client is not None:
55
- return
56
- if os.getenv("ENV") != "production":
57
- load_dotenv()
58
- supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
59
- supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
60
- if not supabase_url or not supabase_key:
61
- raise RuntimeError("SUPABASE_URL SUPABASE_KEY가 필요합니다")
62
- _supabase_client = create_client(supabase_url, supabase_key)
76
+ """환경변수 로드 및 Supabase 클라이언트 초기화"""
77
+ global _supabase_client
78
+ if _supabase_client is not None:
79
+ return
80
+ if os.getenv("ENV") != "production":
81
+ load_dotenv()
82
+ supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
83
+ supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
84
+ if not supabase_url or not supabase_key:
85
+ raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
86
+ _supabase_client = create_client(supabase_url, supabase_key)
63
87
 
64
88
 
65
89
  def get_db_client() -> Client:
66
- if _supabase_client is None:
67
- raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
68
- return _supabase_client
90
+ """초기화된 Supabase 클라이언트 반환."""
91
+ if _supabase_client is None:
92
+ raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
93
+ return _supabase_client
69
94
 
70
95
 
71
96
  def get_consumer_id() -> str:
72
- env_consumer = os.getenv("CONSUMER_ID")
73
- if env_consumer:
74
- return env_consumer
75
- host = socket.gethostname()
76
- pid = os.getpid()
77
- return f"{host}:{pid}"
78
-
79
-
97
+ """파드/프로세스 식별자 생성(CONSUMER_ID>HOST:PID)."""
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
+
106
+ # ------------------------------
107
+ # 폴링: 대기 작업 1건 클레임(RPC 내부에서 상태 변경 포함)
108
+ # ------------------------------
80
109
  async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
81
- def _call():
82
- client = get_db_client()
83
- return client.rpc(
84
- "fetch_pending_task",
85
- {"p_agent_orch": agent_orch, "p_consumer": consumer, "p_limit": 1},
86
- ).execute()
87
-
88
- resp = await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
89
- if not resp or not resp.data:
90
- return None
91
- return resp.data[0]
92
-
93
-
110
+ """대기중 작업 하나를 RPC로 클레임하고 반환. 실패/없음 시 None."""
111
+ def _call():
112
+ client = get_db_client()
113
+ return client.rpc(
114
+ "fetch_pending_task",
115
+ {"p_agent_orch": agent_orch, "p_consumer": consumer, "p_limit": 1},
116
+ ).execute()
117
+
118
+ resp = await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
119
+ if not resp or not resp.data:
120
+ return None
121
+ return resp.data[0]
122
+
123
+
124
+ # ------------------------------
125
+ # 단건 todo 조회
126
+ # ------------------------------
94
127
  async def fetch_todo_by_id(todo_id: str) -> Optional[Dict[str, Any]]:
95
- if not todo_id:
96
- return None
97
- def _call():
98
- client = get_db_client()
99
- return (
100
- client.table("todolist").select("*").eq("id", todo_id).single().execute()
101
- )
102
-
103
- resp = await _async_retry(_call, name="fetch_todo_by_id")
104
- if not resp or not resp.data:
105
- return None
106
- return resp.data
107
-
108
-
128
+ """todolist에서 특정 id의 row 단건 조회. 실패 시 None."""
129
+ if not todo_id:
130
+ return None
131
+ def _call():
132
+ client = get_db_client()
133
+ return (
134
+ client.table("todolist").select("*").eq("id", todo_id).single().execute()
135
+ )
136
+
137
+ resp = await _async_retry(_call, name="fetch_todo_by_id")
138
+ if not resp or not resp.data:
139
+ return None
140
+ return resp.data
141
+
142
+
143
+ # ------------------------------
144
+ # 이벤트 기록
145
+ # ------------------------------
109
146
  async def record_event(todo: Dict[str, Any], data: Dict[str, Any], event_type: Optional[str] = None) -> None:
110
- def _call():
111
- client = get_db_client()
112
- payload: Dict[str, Any] = {
113
- "id": str(uuid.uuid4()),
114
- "job_id": todo.get("proc_inst_id") or str(todo.get("id")),
115
- "todo_id": str(todo.get("id")),
116
- "proc_inst_id": todo.get("proc_inst_id"),
117
- "crew_type": todo.get("agent_orch"),
118
- "data": data,
119
- }
120
- if event_type is not None:
121
- payload["event_type"] = event_type
122
- return client.table("events").insert(payload).execute()
123
-
124
- resp = await _async_retry(_call, name="record_event", fallback=lambda: None)
125
- if resp is None:
126
- _emit_log("record_event 최종 실패(무시)", level=logging.WARNING)
127
-
128
-
147
+ """UI용 events 테이블에 이벤트 기록. 실패해도 플로우 지속."""
148
+ def _call():
149
+ client = get_db_client()
150
+ payload: Dict[str, Any] = {
151
+ "id": str(uuid.uuid4()),
152
+ "job_id": todo.get("proc_inst_id") or str(todo.get("id")),
153
+ "todo_id": str(todo.get("id")),
154
+ "proc_inst_id": todo.get("proc_inst_id"),
155
+ "crew_type": todo.get("agent_orch"),
156
+ "data": data,
157
+ }
158
+ if event_type is not None:
159
+ payload["event_type"] = event_type
160
+ return client.table("events").insert(payload).execute()
161
+
162
+ resp = await _async_retry(_call, name="record_event", fallback=lambda: None)
163
+ if resp is None:
164
+ _emit_log("record_event 최종 실패(무시)", level=logging.WARNING)
165
+
166
+
167
+ # ------------------------------
168
+ # 완료된 데이터 조회
169
+ # ------------------------------
129
170
  async def fetch_done_data(proc_inst_id: Optional[str]) -> List[Any]:
130
- if not proc_inst_id:
131
- return []
132
- def _call():
133
- client = get_db_client()
134
- return client.rpc("fetch_done_data", {"p_proc_inst_id": proc_inst_id}).execute()
135
-
136
- resp = await _async_retry(_call, name="fetch_done_data", fallback=lambda: None)
137
- if not resp:
138
- return []
139
- return [row.get("output") for row in (resp.data or [])]
140
-
141
-
171
+ """같은 proc_inst_id의 완료 output 목록 조회. 실패 시 []."""
172
+ if not proc_inst_id:
173
+ return []
174
+ def _call():
175
+ client = get_db_client()
176
+ return client.rpc("fetch_done_data", {"p_proc_inst_id": proc_inst_id}).execute()
177
+
178
+ resp = await _async_retry(_call, name="fetch_done_data", fallback=lambda: None)
179
+ if not resp:
180
+ return []
181
+ return [row.get("output") for row in (resp.data or [])]
182
+
183
+
184
+ # ------------------------------
185
+ # 결과 저장 (중간/최종)
186
+ # ------------------------------
142
187
  async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
143
- def _call():
144
- client = get_db_client()
145
- payload = result if isinstance(result, (dict, list)) else json.loads(json.dumps(result))
146
- return client.rpc(
147
- "save_task_result",
148
- {"p_todo_id": todo_id, "p_payload": payload, "p_final": final},
149
- ).execute()
188
+ """결과 저장 RPC(중간/최종). 실패해도 경고 로그 후 지속."""
189
+ def _call():
190
+ client = get_db_client()
191
+ payload = result if isinstance(result, (dict, list)) else json.loads(json.dumps(result))
192
+ return client.rpc(
193
+ "save_task_result",
194
+ {"p_todo_id": todo_id, "p_payload": payload, "p_final": final},
195
+ ).execute()
150
196
 
151
- await _async_retry(_call, name="save_task_result", fallback=lambda: None)
197
+ await _async_retry(_call, name="save_task_result", fallback=lambda: None)
152
198
 
153
199
 
200
+ # ------------------------------
201
+ # 추가 유틸: 이벤트/작업/사용자/에이전트/폼/테넌트 조회
202
+ # ------------------------------
154
203
  async def fetch_human_response(job_id: str) -> Optional[Dict[str, Any]]:
155
- def _call():
156
- client = get_db_client()
157
- return (
158
- client.table("events")
159
- .select("*")
160
- .eq("job_id", job_id)
161
- .eq("event_type", "human_response")
162
- .execute()
163
- )
164
-
165
- resp = await _async_retry(_call, name="fetch_human_response")
166
- if not resp or not resp.data:
167
- return None
168
- return resp.data[0]
204
+ """events에서 특정 job_id의 human_response 1건 조회. 실패 시 None."""
205
+ def _call():
206
+ client = get_db_client()
207
+ return (
208
+ client.table("events")
209
+ .select("*")
210
+ .eq("job_id", job_id)
211
+ .eq("event_type", "human_response")
212
+ .execute()
213
+ )
214
+
215
+ resp = await _async_retry(_call, name="fetch_human_response")
216
+ if not resp or not resp.data:
217
+ return None
218
+ return resp.data[0]
169
219
 
170
220
 
171
221
  async def fetch_task_status(todo_id: str) -> Optional[str]:
172
- def _call():
173
- client = get_db_client()
174
- return (
175
- client.table("todolist").select("draft_status").eq("id", todo_id).single().execute()
176
- )
222
+ """todolist.draft_status 조회. 실패 시 None."""
223
+ def _call():
224
+ client = get_db_client()
225
+ return (
226
+ client.table("todolist").select("draft_status").eq("id", todo_id).single().execute()
227
+ )
177
228
 
178
- resp = await _async_retry(_call, name="fetch_task_status")
179
- if not resp or not resp.data:
180
- return None
181
- return resp.data.get("draft_status")
229
+ resp = await _async_retry(_call, name="fetch_task_status")
230
+ if not resp or not resp.data:
231
+ return None
232
+ return resp.data.get("draft_status")
182
233
 
183
234
 
184
235
  async def fetch_all_agents() -> List[Dict[str, Any]]:
185
- def _call():
186
- client = get_db_client()
187
- return (
188
- client.table("users")
189
- .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
190
- .eq("is_agent", True)
191
- .execute()
192
- )
193
-
194
- resp = await _async_retry(_call, name="fetch_all_agents")
195
- rows = resp.data or [] if resp else []
196
- normalized: List[Dict[str, Any]] = []
197
- for row in rows:
198
- normalized.append(
199
- {
200
- "id": row.get("id"),
201
- "name": row.get("username"),
202
- "role": row.get("role"),
203
- "goal": row.get("goal"),
204
- "persona": row.get("persona"),
205
- "tools": row.get("tools") or "mem0",
206
- "profile": row.get("profile"),
207
- "model": row.get("model"),
208
- "tenant_id": row.get("tenant_id"),
209
- }
210
- )
211
- return normalized
236
+ """모든 에이전트 목록 정규화 반환. 실패 시 []."""
237
+ def _call():
238
+ client = get_db_client()
239
+ return (
240
+ client.table("users")
241
+ .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
242
+ .eq("is_agent", True)
243
+ .execute()
244
+ )
245
+
246
+ resp = await _async_retry(_call, name="fetch_all_agents")
247
+ rows = resp.data or [] if resp else []
248
+ normalized: List[Dict[str, Any]] = []
249
+ for row in rows:
250
+ normalized.append(
251
+ {
252
+ "id": row.get("id"),
253
+ "name": row.get("username"),
254
+ "role": row.get("role"),
255
+ "goal": row.get("goal"),
256
+ "persona": row.get("persona"),
257
+ "tools": row.get("tools") or "mem0",
258
+ "profile": row.get("profile"),
259
+ "model": row.get("model"),
260
+ "tenant_id": row.get("tenant_id"),
261
+ }
262
+ )
263
+ return normalized
212
264
 
213
265
 
214
266
  async def fetch_agent_data(user_ids: str) -> List[Dict[str, Any]]:
215
- def _is_valid_uuid(value: str) -> bool:
216
- try:
217
- uuid.UUID(str(value))
218
- return True
219
- except Exception:
220
- return False
221
-
222
- raw_ids = [x.strip() for x in (user_ids or "").split(",") if x.strip()]
223
- valid_ids = [x for x in raw_ids if _is_valid_uuid(x)]
224
-
225
- if not valid_ids:
226
- return await fetch_all_agents()
227
-
228
- def _call():
229
- client = get_db_client()
230
- resp = (
231
- client
232
- .table("users")
233
- .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
234
- .in_("id", valid_ids)
235
- .eq("is_agent", True)
236
- .execute()
237
- )
238
- rows = resp.data or []
239
- normalized: List[Dict[str, Any]] = []
240
- for row in rows:
241
- normalized.append(
242
- {
243
- "id": row.get("id"),
244
- "name": row.get("username"),
245
- "role": row.get("role"),
246
- "goal": row.get("goal"),
247
- "persona": row.get("persona"),
248
- "tools": row.get("tools") or "mem0",
249
- "profile": row.get("profile"),
250
- "model": row.get("model"),
251
- "tenant_id": row.get("tenant_id"),
252
- }
253
- )
254
- return normalized
255
-
256
- result = await _async_retry(_call, name="fetch_agent_data", fallback=lambda: [])
257
-
258
- if not result:
259
- return await fetch_all_agents()
260
-
261
- return result
267
+ """user_id() 에이전트를 조회. 없거나 유효하지 않으면 모든 에이전트를 반환.
268
+
269
+ - 입력은 UUID 또는 콤마(,)로 구분된 UUID 목록을 허용
270
+ - 유효한 UUID가 하나도 없으면 전체 에이전트 반환
271
+ - 유효한 UUID로 조회했는데 결과가 비면 전체 에이전트 반환
272
+ """
273
+ def _is_valid_uuid(value: str) -> bool:
274
+ try:
275
+ uuid.UUID(str(value))
276
+ return True
277
+ except Exception:
278
+ return False
279
+
280
+ # 1) 입력 정규화 및 UUID 필터링
281
+ raw_ids = [x.strip() for x in (user_ids or "").split(",") if x.strip()]
282
+ valid_ids = [x for x in raw_ids if _is_valid_uuid(x)]
283
+
284
+ # 2) 유효한 UUID가 없으면 전체 에이전트 반환
285
+ if not valid_ids:
286
+ return await fetch_all_agents()
287
+
288
+ # 3) 유효한 UUID로 에이전트 조회
289
+ def _call():
290
+ client = get_db_client()
291
+ resp = (
292
+ client
293
+ .table("users")
294
+ .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
295
+ .in_("id", valid_ids)
296
+ .eq("is_agent", True)
297
+ .execute()
298
+ )
299
+ rows = resp.data or []
300
+ normalized: List[Dict[str, Any]] = []
301
+ for row in rows:
302
+ normalized.append(
303
+ {
304
+ "id": row.get("id"),
305
+ "name": row.get("username"),
306
+ "role": row.get("role"),
307
+ "goal": row.get("goal"),
308
+ "persona": row.get("persona"),
309
+ "tools": row.get("tools") or "mem0",
310
+ "profile": row.get("profile"),
311
+ "model": row.get("model"),
312
+ "tenant_id": row.get("tenant_id"),
313
+ }
314
+ )
315
+ return normalized
316
+
317
+ result = await _async_retry(_call, name="fetch_agent_data", fallback=lambda: [])
318
+
319
+ # 4) 결과가 없으면 전체 에이전트로 폴백
320
+ if not result:
321
+ return await fetch_all_agents()
322
+
323
+ return result
262
324
 
263
325
 
264
326
  async def fetch_form_types(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]]]:
265
- def _call():
266
- client = get_db_client()
267
- form_id = tool_val[12:] if tool_val.startswith("formHandler:") else tool_val
268
- resp = (
269
- client.table("form_def").select("fields_json").eq("id", form_id).eq("tenant_id", tenant_id).execute()
270
- )
271
- fields_json = resp.data[0].get("fields_json") if resp.data else None
272
- if not fields_json:
273
- return form_id, [{"key": form_id, "type": "default", "text": ""}]
274
- return form_id, fields_json
275
-
276
- resp = await _async_retry(_call, name="fetch_form_types", fallback=lambda: (tool_val, [{"key": tool_val, "type": "default", "text": ""}]))
277
- return resp if resp else (tool_val, [{"key": tool_val, "type": "default", "text": ""}])
327
+ """폼 타입 정의 조회 및 정규화. 실패 시 기본값 반환."""
328
+ def _call():
329
+ client = get_db_client()
330
+ form_id = tool_val[12:] if tool_val.startswith("formHandler:") else tool_val
331
+ resp = (
332
+ client.table("form_def").select("fields_json").eq("id", form_id).eq("tenant_id", tenant_id).execute()
333
+ )
334
+ fields_json = resp.data[0].get("fields_json") if resp.data else None
335
+ if not fields_json:
336
+ return form_id, [{"key": form_id, "type": "default", "text": ""}]
337
+ return form_id, fields_json
338
+
339
+ resp = await _async_retry(_call, name="fetch_form_types", fallback=lambda: (tool_val, [{"key": tool_val, "type": "default", "text": ""}]))
340
+ return resp if resp else (tool_val, [{"key": tool_val, "type": "default", "text": ""}])
278
341
 
279
342
 
280
343
  async def fetch_tenant_mcp_config(tenant_id: str) -> Optional[Dict[str, Any]]:
281
- def _call():
282
- client = get_db_client()
283
- return client.table("tenants").select("mcp").eq("id", tenant_id).single().execute()
284
- try:
285
- resp = await _async_retry(_call, name="fetch_tenant_mcp_config", fallback=lambda: None)
286
- return resp.data.get("mcp") if resp and resp.data else None
287
- except Exception as e:
288
- _emit_error("fetch_tenant_mcp_config 실패", e, raise_error=False)
289
- return None
344
+ """테넌트 MCP 설정 조회. 실패 시 None."""
345
+ def _call():
346
+ client = get_db_client()
347
+ return client.table("tenants").select("mcp").eq("id", tenant_id).single().execute()
348
+ try:
349
+ resp = await _async_retry(_call, name="fetch_tenant_mcp_config", fallback=lambda: None)
350
+ return resp.data.get("mcp") if resp and resp.data else None
351
+ except Exception as e:
352
+ _emit_error("fetch_tenant_mcp_config 실패", e, raise_error=False)
353
+ return None
354
+
355
+
356
+ # ------------------------------
357
+ # 오류 상태 업데이트 (FAILED)
358
+ # ------------------------------
359
+ async def update_task_error(todo_id: str) -> None:
360
+ """작업 오류 상태 업데이트 (FAILED) - 로그 컬럼은 건드리지 않음"""
361
+ if not todo_id:
362
+ return
363
+ def _call():
364
+ client = get_db_client()
365
+ return (
366
+ client
367
+ .table('todolist')
368
+ .update({'draft_status': 'FAILED', 'consumer': None})
369
+ .eq('id', todo_id)
370
+ .execute()
371
+ )
372
+
373
+ await _async_retry(_call, name="update_task_error", fallback=lambda: None)
@@ -13,11 +13,13 @@ from .core.database import (
13
13
  fetch_form_types,
14
14
  fetch_task_status,
15
15
  fetch_tenant_mcp_config,
16
+ update_task_error,
16
17
  )
17
18
 
18
19
  from .utils.logger import handle_error as _emit_error, log as _emit_log
19
20
  from .utils.summarizer import summarize_async
20
21
  from .utils.event_handler import route_event
22
+ from .utils.context_manager import set_context, reset_context
21
23
 
22
24
 
23
25
  class ProcessGPTAgentServer:
@@ -49,14 +51,23 @@ class ProcessGPTAgentServer:
49
51
  todo_id = todo["id"]
50
52
  _emit_log(f"[JOB START] todo_id={todo_id}")
51
53
 
52
- prepared_data = await self._prepare_service_data(todo)
53
- _emit_log(f"[RUN] 서비스 데이터 준비 완료 [todo_id={todo_id} agent={prepared_data.get('agent_orch','')}]")
54
+ try:
55
+ prepared_data = await self._prepare_service_data(todo)
56
+ _emit_log(f"[RUN] 서비스 데이터 준비 완료 [todo_id={todo_id} agent={prepared_data.get('agent_orch','')}]")
54
57
 
55
- await self._execute_with_cancel_watch(todo, prepared_data)
56
- _emit_log(f"[RUN] 서비스 실행 완료 [todo_id={todo_id} agent={prepared_data.get('agent_orch','')}]")
58
+ await self._execute_with_cancel_watch(todo, prepared_data)
59
+ _emit_log(f"[RUN] 서비스 실행 완료 [todo_id={todo_id} agent={prepared_data.get('agent_orch','')}]")
60
+ except Exception as job_err:
61
+ _emit_error("작업 처리 오류", job_err, raise_error=False)
62
+ try:
63
+ await update_task_error(str(todo_id))
64
+ except Exception as upd_err:
65
+ _emit_error("FAILED 상태 업데이트 실패", upd_err, raise_error=False)
66
+ # 다음 루프로 진행
67
+ continue
57
68
 
58
69
  except Exception as e:
59
- _emit_error("폴링 루프 오류", e)
70
+ _emit_error("폴링 루프 오류", e, raise_error=False)
60
71
  await asyncio.sleep(self.polling_interval)
61
72
 
62
73
  def stop(self) -> None:
@@ -108,6 +119,17 @@ class ProcessGPTAgentServer:
108
119
  context = ProcessGPTRequestContext(prepared_data)
109
120
  event_queue = ProcessGPTEventQueue(todo)
110
121
 
122
+ # 실행 전 컨텍스트 변수 설정 (CrewAI 전역 리스너 등에서 활용)
123
+ try:
124
+ set_context(
125
+ todo_id=str(todo.get("id")),
126
+ proc_inst_id=str(todo.get("proc_inst_id") or ""),
127
+ crew_type=str(prepared_data.get("agent_orch") or ""),
128
+ form_id=str(prepared_data.get("form_id") or ""),
129
+ )
130
+ except Exception as e:
131
+ _emit_error("컨텍스트 설정 실패", e, raise_error=False)
132
+
111
133
  _emit_log(f"[EXEC START] todo_id={todo.get('id')} agent={prepared_data.get('agent_orch','')}")
112
134
  execute_task = asyncio.create_task(executor.execute(context, event_queue))
113
135
  cancel_watch_task = asyncio.create_task(self._watch_cancellation(todo, executor, context, event_queue, execute_task))
@@ -121,14 +143,19 @@ class ProcessGPTAgentServer:
121
143
  task.cancel()
122
144
 
123
145
  except Exception as e:
124
- _emit_error("서비스 실행 오류", e)
146
+ _emit_error("서비스 실행 오류", e, raise_error=False)
125
147
  cancel_watch_task.cancel()
126
148
  execute_task.cancel()
127
149
  finally:
150
+ # 컨텍스트 정리
151
+ try:
152
+ reset_context()
153
+ except Exception as e:
154
+ _emit_error("컨텍스트 리셋 실패", e, raise_error=False)
128
155
  try:
129
156
  await event_queue.close()
130
157
  except Exception as e:
131
- _emit_error("이벤트 큐 종료 실패", e)
158
+ _emit_error("이벤트 큐 종료 실패", e, raise_error=False)
132
159
  _emit_log(f"[EXEC END] todo_id={todo.get('id')} agent={prepared_data.get('agent_orch','')}")
133
160
 
134
161
  async def _watch_cancellation(self, todo: Dict[str, Any], executor: AgentExecutor, context: RequestContext, event_queue: EventQueue, execute_task: asyncio.Task) -> None:
@@ -145,16 +172,16 @@ class ProcessGPTAgentServer:
145
172
  try:
146
173
  await executor.cancel(context, event_queue)
147
174
  except Exception as e:
148
- _emit_error("취소 처리 실패", e)
175
+ _emit_error("취소 처리 실패", e, raise_error=False)
149
176
  finally:
150
177
  try:
151
178
  execute_task.cancel()
152
179
  except Exception as e:
153
- _emit_error("실행 태스크 즉시 취소 실패", e)
180
+ _emit_error("실행 태스크 즉시 취소 실패", e, raise_error=False)
154
181
  try:
155
182
  await event_queue.close()
156
183
  except Exception as e:
157
- _emit_error("취소 후 이벤트 큐 종료 실패", e)
184
+ _emit_error("취소 후 이벤트 큐 종료 실패", e, raise_error=False)
158
185
  break
159
186
 
160
187
 
@@ -189,17 +216,17 @@ class ProcessGPTEventQueue(EventQueue):
189
216
  try:
190
217
  super().enqueue_event(event)
191
218
  except Exception as e:
192
- _emit_error("이벤트 큐 삽입 실패", e)
219
+ _emit_error("이벤트 큐 삽입 실패", e, raise_error=False)
193
220
 
194
221
  self._create_bg_task(route_event(self.todo, event), "route_event")
195
222
  except Exception as e:
196
- _emit_error("이벤트 저장 실패", e)
223
+ _emit_error("이벤트 저장 실패", e, raise_error=False)
197
224
 
198
225
  def task_done(self) -> None:
199
226
  try:
200
227
  _emit_log(f"태스크 완료: {self.todo['id']}")
201
228
  except Exception as e:
202
- _emit_error("태스크 완료 처리 실패", e)
229
+ _emit_error("태스크 완료 처리 실패", e, raise_error=False)
203
230
 
204
231
  async def close(self) -> None:
205
232
  pass
@@ -210,7 +237,7 @@ class ProcessGPTEventQueue(EventQueue):
210
237
  def _cb(t: asyncio.Task):
211
238
  exc = t.exception()
212
239
  if exc:
213
- _emit_error(f"백그라운드 태스크 오류({label})", exc)
240
+ _emit_error(f"백그라운드 태스크 오류({label})", exc, raise_error=False)
214
241
  task.add_done_callback(_cb)
215
242
  except Exception as e:
216
- _emit_error(f"백그라운드 태스크 생성 실패({label})", e)
243
+ _emit_error(f"백그라운드 태스크 생성 실패({label})", e, raise_error=False)
@@ -186,6 +186,6 @@ class SafeToolLoader:
186
186
  try:
187
187
  adapter.stop()
188
188
  except Exception as e:
189
- handle_error("툴종료오류", e, raise_error=True)
189
+ handle_error("툴종료오류", e, raise_error=False)
190
190
  log("모든 MCPServerAdapter 연결 종료 완료")
191
191
  cls.adapters.clear()
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar
4
+ from typing import Optional
5
+
6
+
7
+ todo_id_var: ContextVar[Optional[str]] = ContextVar("todo_id", default=None)
8
+ proc_id_var: ContextVar[Optional[str]] = ContextVar("proc_id", default=None)
9
+ crew_type_var: ContextVar[Optional[str]] = ContextVar("crew_type", default=None)
10
+ form_key_var: ContextVar[Optional[str]] = ContextVar("form_key", default=None)
11
+ form_id_var: ContextVar[Optional[str]] = ContextVar("form_id", default=None)
12
+
13
+
14
+ def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] = None, crew_type: Optional[str] = None, form_key: Optional[str] = None, form_id: Optional[str] = None) -> None:
15
+ if todo_id is not None:
16
+ todo_id_var.set(todo_id)
17
+ if proc_inst_id is not None:
18
+ proc_id_var.set(proc_inst_id)
19
+ if crew_type is not None:
20
+ crew_type_var.set(crew_type)
21
+ if form_key is not None:
22
+ form_key_var.set(form_key)
23
+ if form_id is not None:
24
+ form_id_var.set(form_id)
25
+
26
+
27
+ def reset_context() -> None:
28
+ todo_id_var.set(None)
29
+ proc_id_var.set(None)
30
+ crew_type_var.set(None)
31
+ form_key_var.set(None)
32
+ form_id_var.set(None)
33
+
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import json
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Optional, Dict, List
8
+
9
+ from crewai.utilities.events import CrewAIEventsBus, ToolUsageStartedEvent, ToolUsageFinishedEvent
10
+ from crewai.utilities.events.task_events import TaskStartedEvent, TaskCompletedEvent
11
+
12
+ from .logger import handle_error as _err, log as _log
13
+ from .context_manager import todo_id_var, proc_id_var, crew_type_var, form_id_var, form_key_var
14
+ from ..core.database import initialize_db, get_db_client
15
+
16
+
17
+ class CrewAIEventLogger:
18
+ """CrewAI 이벤트 로거 - Supabase 전용"""
19
+
20
+ # =============================================================================
21
+ # Initialization
22
+ # =============================================================================
23
+ def __init__(self):
24
+ initialize_db()
25
+ self.supabase = get_db_client()
26
+ _log("CrewAIEventLogger 초기화 완료")
27
+
28
+ # =============================================================================
29
+ # Job ID Generation
30
+ # =============================================================================
31
+ def _generate_job_id(self, event_obj: Any, source: Any = None) -> str:
32
+ """이벤트 객체에서 Job ID 생성"""
33
+ try:
34
+ if hasattr(event_obj, "task") and hasattr(event_obj.task, "id"):
35
+ return str(event_obj.task.id)
36
+ if source and hasattr(source, "task") and hasattr(source.task, "id"):
37
+ return str(source.task.id)
38
+ except Exception:
39
+ pass
40
+ return "unknown"
41
+
42
+ # =============================================================================
43
+ # Record Creation
44
+ # =============================================================================
45
+ def _create_event_record(
46
+ self,
47
+ event_type: str,
48
+ data: Dict[str, Any],
49
+ job_id: str,
50
+ crew_type: str,
51
+ todo_id: Optional[str],
52
+ proc_inst_id: Optional[str],
53
+ ) -> Dict[str, Any]:
54
+ """이벤트 레코드 생성"""
55
+ return {
56
+ "id": str(uuid.uuid4()),
57
+ "job_id": job_id,
58
+ "todo_id": todo_id,
59
+ "proc_inst_id": proc_inst_id,
60
+ "event_type": event_type,
61
+ "crew_type": crew_type,
62
+ "data": data,
63
+ "timestamp": datetime.now(timezone.utc).isoformat(),
64
+ }
65
+
66
+ # =============================================================================
67
+ # Parsing Helpers
68
+ # =============================================================================
69
+ def _parse_json_text(self, text: str) -> Any:
70
+ """JSON 문자열을 객체로 파싱하거나 원본 반환"""
71
+ try:
72
+ return json.loads(text)
73
+ except:
74
+ return text
75
+
76
+ def _parse_output(self, output: Any) -> Any:
77
+ """output 또는 raw 텍스트를 파싱해 반환"""
78
+ if not output:
79
+ return ""
80
+ text = getattr(output, "raw", None) or (output if isinstance(output, str) else "")
81
+ return self._parse_json_text(text)
82
+
83
+ def _parse_tool_args(self, args_text: str) -> Optional[str]:
84
+ """tool_args에서 query 키 추출"""
85
+ try:
86
+ args = json.loads(args_text or "{}")
87
+ return args.get("query")
88
+ except Exception:
89
+ return None
90
+
91
+ # =============================================================================
92
+ # Formatting Helpers
93
+ # =============================================================================
94
+ def _format_plans_md(self, plans: List[Dict[str, Any]]) -> str:
95
+ """list_of_plans_per_task 형식을 Markdown 문자열로 변환"""
96
+ lines: List[str] = []
97
+ for idx, item in enumerate(plans or [], 1):
98
+ task = item.get("task", "")
99
+ plan = item.get("plan", "")
100
+ lines.append(f"## {idx}. {task}")
101
+ lines.append("")
102
+ if isinstance(plan, list):
103
+ for line in plan:
104
+ lines.append(str(line))
105
+ elif isinstance(plan, str):
106
+ for line in plan.split("\n"):
107
+ lines.append(line)
108
+ else:
109
+ lines.append(str(plan))
110
+ lines.append("")
111
+ return "\n".join(lines).strip()
112
+
113
+ # =============================================================================
114
+ # Data Extraction
115
+ # =============================================================================
116
+ def _extract_event_data(self, event_obj: Any, source: Any = None) -> Dict[str, Any]:
117
+ """이벤트 타입별 데이터 추출"""
118
+ etype = getattr(event_obj, "type", None) or type(event_obj).__name__
119
+ if etype == "task_started":
120
+ agent = getattr(getattr(event_obj, "task", None), "agent", None)
121
+ return {
122
+ "role": getattr(agent, "role", "Unknown"),
123
+ "goal": getattr(agent, "goal", "Unknown"),
124
+ "agent_profile": getattr(agent, "profile", None) or "/images/chat-icon.png",
125
+ "name": getattr(agent, "name", "Unknown"),
126
+ }
127
+ if etype == "task_completed":
128
+ result = self._parse_output(getattr(event_obj, "output", None))
129
+ if isinstance(result, dict) and "list_of_plans_per_task" in result:
130
+ md = self._format_plans_md(result.get("list_of_plans_per_task") or [])
131
+ return {"plans": md}
132
+ return {"result": result}
133
+
134
+ if etype in ("tool_usage_started", "tool_usage_finished") or str(etype).startswith("tool_"):
135
+ return {
136
+ "tool_name": getattr(event_obj, "tool_name", None),
137
+ "query": self._parse_tool_args(getattr(event_obj, "tool_args", "")),
138
+ }
139
+ return {"info": f"Event type: {etype}"}
140
+
141
+ # =============================================================================
142
+ # Event Saving
143
+ # =============================================================================
144
+ def _save_event(self, record: Dict[str, Any]) -> None:
145
+ """Supabase에 이벤트 레코드 저장 (간단 재시도 포함)"""
146
+ payload = json.loads(json.dumps(record, default=str))
147
+ for attempt in range(1, 4):
148
+ try:
149
+ self.supabase.table("events").insert(payload).execute()
150
+ return
151
+ except Exception as e:
152
+ if attempt < 3:
153
+ _err("이벤트저장오류(재시도)", e, raise_error=False)
154
+ # 지수 백오프: 0.3s, 0.6s
155
+ import time
156
+ time.sleep(0.3 * attempt)
157
+ continue
158
+ _err("이벤트저장오류(최종)", e, raise_error=False)
159
+ return
160
+
161
+ # =============================================================================
162
+ # Event Handling
163
+ # =============================================================================
164
+ def on_event(self, event_obj: Any, source: Any = None) -> None:
165
+ """이벤트 수신부터 DB 저장까지 처리"""
166
+ etype = getattr(event_obj, "type", None) or type(event_obj).__name__
167
+ ALLOWED = {"task_started", "task_completed", "tool_usage_started", "tool_usage_finished"}
168
+ if etype not in ALLOWED:
169
+ return
170
+ try:
171
+ job_id = self._generate_job_id(event_obj, source)
172
+ data = self._extract_event_data(event_obj, source)
173
+ crew_type = crew_type_var.get() or "action"
174
+ rec = self._create_event_record(etype, data, job_id, crew_type, todo_id_var.get(), proc_id_var.get())
175
+ self._save_event(rec)
176
+ _log(f"[{etype}] [{job_id[:8]}] 저장 완료")
177
+ except Exception as e:
178
+ _err("이벤트처리오류", e, raise_error=False)
179
+
180
+
181
+
182
+ class CrewConfigManager:
183
+ """글로벌 CrewAI 이벤트 리스너 등록 매니저"""
184
+ _registered_by_pid: set[int] = set()
185
+
186
+ def __init__(self) -> None:
187
+ self.logger = CrewAIEventLogger()
188
+ self._register_once_per_process()
189
+
190
+ def _register_once_per_process(self) -> None:
191
+ try:
192
+ pid = os.getpid()
193
+ if pid in self._registered_by_pid:
194
+ return
195
+ bus = CrewAIEventsBus()
196
+ for evt in (TaskStartedEvent, TaskCompletedEvent, ToolUsageStartedEvent, ToolUsageFinishedEvent):
197
+ bus.on(evt)(lambda source, event, logger=self.logger: logger.on_event(event, source))
198
+ self._registered_by_pid.add(pid)
199
+ _log("CrewAI event listeners 등록 완료")
200
+ except Exception as e:
201
+ _err("CrewAI 이벤트 버스 등록 실패", e, raise_error=False)
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ import uuid
5
+ from datetime import datetime, timezone
3
6
  from typing import Any, Dict
4
7
 
5
8
  from a2a.server.events import Event
6
9
  from .logger import handle_error as _emit_error, log as _emit_log
7
- from ..core.database import record_event
10
+ from ..core.database import record_event, save_task_result
8
11
 
9
12
 
10
13
  def _event_to_dict(event: Event) -> Dict[str, Any]:
@@ -13,15 +16,33 @@ def _event_to_dict(event: Event) -> Dict[str, Any]:
13
16
  return {k: v for k, v in event.__dict__.items() if not k.startswith("_")}
14
17
  return {"event": str(event)}
15
18
  except Exception as e:
16
- _emit_error("event dict 변환 실패", e)
19
+ _emit_error("event dict 변환 실패", e, raise_error=False)
17
20
  return {"event": str(event)}
18
21
 
19
22
 
20
23
  async def route_event(todo: Dict[str, Any], event: Event) -> None:
21
- """이벤트를 dict으로 변환해 events 테이블에 기록.
24
+ """이벤트(dict)와 출력(output)을 구분해 처리.
22
25
 
23
- - 복잡한 라우팅/분기는 추후 확장. 현재는 단일 events 테이블에 기록.
24
- - event_type은 None으로 두거나, 필요 상위에서 지정하도록 추후 확장 가능.
26
+ - "event": CrewAI 등에서 발생한 실행 이벤트 events 테이블 저장
27
+ - "output": 실행 결과 save_task_result를 통해 중간/최종 여부에 따라 저장
25
28
  """
26
- data = _event_to_dict(event)
27
- await record_event(todo, data, event_type=None)
29
+ try:
30
+ data = _event_to_dict(event)
31
+
32
+ # output 이벤트 처리
33
+ if data.get("type") == "output" or data.get("event_type") == "output":
34
+ payload = data.get("data") or data.get("payload") or {}
35
+ is_final = bool(payload.get("final") or payload.get("is_final"))
36
+ content = payload.get("content") if isinstance(payload, dict) else payload
37
+ await save_task_result(str(todo.get("id")), content, final=is_final)
38
+ return
39
+
40
+ # 일반 event 처리 (원형 + 타임스탬프)
41
+ normalized = {
42
+ "id": str(uuid.uuid4()),
43
+ "timestamp": datetime.now(timezone.utc).isoformat(),
44
+ **data,
45
+ }
46
+ await record_event(todo, normalized, event_type=str(data.get("type") or data.get("event_type") or "event"))
47
+ except Exception as e:
48
+ _emit_error("route_event 처리 실패", e, raise_error=False)
@@ -1,30 +1,30 @@
1
- import logging
2
- import os
3
- import traceback
4
- from typing import Optional, Dict
5
-
6
-
7
- # Configure root logger only once (idempotent)
8
- if not logging.getLogger().handlers:
9
- logging.basicConfig(
10
- level=logging.INFO,
11
- format="%(asctime)s %(levelname)s %(name)s - %(message)s",
12
- )
13
-
14
- _logger = logging.getLogger("process-gpt-agent-framework")
15
-
16
-
17
- def log(message: str, level: int = logging.INFO) -> None:
18
- spaced = os.getenv("LOG_SPACED", "1") != "0"
19
- suffix = "\n" if spaced else ""
20
- _logger.log(level, f"{message}{suffix}")
21
-
22
-
23
- def handle_error(title: str, error: Exception, *, raise_error: bool = False, extra: Optional[Dict] = None) -> None:
24
- spaced = os.getenv("LOG_SPACED", "1") != "0"
25
- suffix = "\n" if spaced else ""
26
- context = f" | extra={extra}" if extra else ""
27
- _logger.error(f"{title}: {error}{context}{suffix}")
28
- _logger.error(traceback.format_exc())
29
- if raise_error:
30
- raise error
1
+ import logging
2
+ import os
3
+ import traceback
4
+ from typing import Optional, Dict
5
+
6
+
7
+ # Configure root logger only once (idempotent)
8
+ if not logging.getLogger().handlers:
9
+ logging.basicConfig(
10
+ level=logging.INFO,
11
+ format="%(asctime)s %(levelname)s %(name)s - %(message)s",
12
+ )
13
+
14
+ _logger = logging.getLogger("process-gpt-agent-framework")
15
+
16
+
17
+ def log(message: str, level: int = logging.INFO) -> None:
18
+ spaced = os.getenv("LOG_SPACED", "1") != "0"
19
+ suffix = "\n" if spaced else ""
20
+ _logger.log(level, f"{message}{suffix}")
21
+
22
+
23
+ def handle_error(title: str, error: Exception, *, raise_error: bool = True, extra: Optional[Dict] = None) -> None:
24
+ spaced = os.getenv("LOG_SPACED", "1") != "0"
25
+ suffix = "\n" if spaced else ""
26
+ context = f" | extra={extra}" if extra else ""
27
+ _logger.error(f"{title}: {error}{context}{suffix}")
28
+ _logger.error(traceback.format_exc())
29
+ if raise_error:
30
+ raise error
@@ -1,15 +0,0 @@
1
- processgpt_agent_sdk/__init__.py,sha256=pvHXYrXIlbyB095vfSZhzTuCUzIZ9_OQHr3xTqzyRvM,192
2
- processgpt_agent_sdk/server.py,sha256=2rZE2jcebxxjQxvEwdrdOpEY4MlEBUsLcDrDnuGvTlw,6939
3
- processgpt_agent_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- processgpt_agent_sdk/core/database.py,sha256=9S8M4asyimwThW2U4Vymlo4w58VedxP4mwV99qcKxQw,8677
5
- processgpt_agent_sdk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- processgpt_agent_sdk/tools/knowledge_tools.py,sha256=-IPQB6A59wMTBsZUDM-7q10IHMz30X-m_vp6RgB6yHA,8995
7
- processgpt_agent_sdk/tools/safe_tool_loader.py,sha256=1Kxzctk2ELVYa1LOGKZxacleeGkhZfxYbjgeh3PHArI,5646
8
- processgpt_agent_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- processgpt_agent_sdk/utils/event_handler.py,sha256=sMu6z6SbnC7HoHL3ItyS3Mpx2MThmbp0rgboJSqYD-Y,924
10
- processgpt_agent_sdk/utils/logger.py,sha256=zm01prh6G6CibiNmf57Of1m-lV2AYzibJO2uKF3xDXc,909
11
- processgpt_agent_sdk/utils/summarizer.py,sha256=XV7e4pyKEK7NiGK_PAc1xorKSO2MKlSHCAiV7slOdus,4527
12
- process_gpt_agent_sdk-0.1.2.dist-info/METADATA,sha256=Fd83G-ptGsYP6KX8_vQiO13ESnnrgCpMmheF_iSRwOk,12896
13
- process_gpt_agent_sdk-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- process_gpt_agent_sdk-0.1.2.dist-info/top_level.txt,sha256=Xe6zrj3_3Vv7d0pl5RRtenVUckwOVBVLQn2P03j5REo,21
15
- process_gpt_agent_sdk-0.1.2.dist-info/RECORD,,