process-gpt-agent-sdk 0.1.2__tar.gz → 0.1.3__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.
- {process_gpt_agent_sdk-0.1.2/process_gpt_agent_sdk.egg-info → process_gpt_agent_sdk-0.1.3}/PKG-INFO +1 -1
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3/process_gpt_agent_sdk.egg-info}/PKG-INFO +1 -1
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/process_gpt_agent_sdk.egg-info/SOURCES.txt +2 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/__init__.py +7 -7
- process_gpt_agent_sdk-0.1.3/processgpt_agent_sdk/core/database.py +373 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/server.py +42 -15
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/tools/safe_tool_loader.py +1 -1
- process_gpt_agent_sdk-0.1.3/processgpt_agent_sdk/utils/context_manager.py +33 -0
- process_gpt_agent_sdk-0.1.3/processgpt_agent_sdk/utils/crewai_event_listener.py +201 -0
- process_gpt_agent_sdk-0.1.3/processgpt_agent_sdk/utils/event_handler.py +48 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/logger.py +30 -30
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/pyproject.toml +1 -1
- process_gpt_agent_sdk-0.1.2/processgpt_agent_sdk/core/database.py +0 -289
- process_gpt_agent_sdk-0.1.2/processgpt_agent_sdk/utils/event_handler.py +0 -27
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/MANIFEST.in +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/README.md +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/function.sql +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/process_gpt_agent_sdk.egg-info/dependency_links.txt +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/process_gpt_agent_sdk.egg-info/requires.txt +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/process_gpt_agent_sdk.egg-info/top_level.txt +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/core/__init__.py +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/tools/__init__.py +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/tools/knowledge_tools.py +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/__init__.py +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/summarizer.py +0 -0
- {process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/setup.cfg +0 -0
|
@@ -15,6 +15,8 @@ processgpt_agent_sdk/tools/__init__.py
|
|
|
15
15
|
processgpt_agent_sdk/tools/knowledge_tools.py
|
|
16
16
|
processgpt_agent_sdk/tools/safe_tool_loader.py
|
|
17
17
|
processgpt_agent_sdk/utils/__init__.py
|
|
18
|
+
processgpt_agent_sdk/utils/context_manager.py
|
|
19
|
+
processgpt_agent_sdk/utils/crewai_event_listener.py
|
|
18
20
|
processgpt_agent_sdk/utils/event_handler.py
|
|
19
21
|
processgpt_agent_sdk/utils/logger.py
|
|
20
22
|
processgpt_agent_sdk/utils/summarizer.py
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/__init__.py
RENAMED
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""ProcessGPT DB utilities
|
|
2
|
+
|
|
3
|
+
역할:
|
|
4
|
+
- Supabase 연결/초기화
|
|
5
|
+
- 안전한 RPC/CRUD 호출(재시도/폴백 포함)
|
|
6
|
+
- 이벤트 기록, 작업 클레임/상태/저장/조회, 사용자·에이전트·폼·테넌트 조회
|
|
7
|
+
|
|
8
|
+
반환 규칙(폴백 포함):
|
|
9
|
+
- Optional 단건 조회 계열 → 실패 시 None
|
|
10
|
+
- 목록/시퀀스 계열 → 실패 시 빈 리스트 []
|
|
11
|
+
- 변경/기록 계열 → 실패 시 경고 로그만 남기고 None
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import json
|
|
16
|
+
import asyncio
|
|
17
|
+
import socket
|
|
18
|
+
import uuid
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from dotenv import load_dotenv
|
|
23
|
+
from supabase import Client, create_client
|
|
24
|
+
import logging
|
|
25
|
+
import random
|
|
26
|
+
from typing import Callable, TypeVar
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
|
|
30
|
+
from ..utils.logger import handle_error as _emit_error, log as _emit_log
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _async_retry(
|
|
34
|
+
fn: Callable[[], T],
|
|
35
|
+
*,
|
|
36
|
+
name: str,
|
|
37
|
+
retries: int = 3,
|
|
38
|
+
base_delay: float = 0.8,
|
|
39
|
+
fallback: Optional[Callable[[], T]] = None,
|
|
40
|
+
) -> Optional[T]:
|
|
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
|
+
# ------------------------------
|
|
72
|
+
_supabase_client: Optional[Client] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def initialize_db() -> None:
|
|
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)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_db_client() -> Client:
|
|
90
|
+
"""초기화된 Supabase 클라이언트 반환."""
|
|
91
|
+
if _supabase_client is None:
|
|
92
|
+
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
93
|
+
return _supabase_client
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_consumer_id() -> str:
|
|
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
|
+
# ------------------------------
|
|
109
|
+
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
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
|
+
# ------------------------------
|
|
127
|
+
async def fetch_todo_by_id(todo_id: str) -> Optional[Dict[str, Any]]:
|
|
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
|
+
# ------------------------------
|
|
146
|
+
async def record_event(todo: Dict[str, Any], data: Dict[str, Any], event_type: Optional[str] = None) -> None:
|
|
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
|
+
# ------------------------------
|
|
170
|
+
async def fetch_done_data(proc_inst_id: Optional[str]) -> List[Any]:
|
|
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
|
+
# ------------------------------
|
|
187
|
+
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
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()
|
|
196
|
+
|
|
197
|
+
await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ------------------------------
|
|
201
|
+
# 추가 유틸: 이벤트/작업/사용자/에이전트/폼/테넌트 조회
|
|
202
|
+
# ------------------------------
|
|
203
|
+
async def fetch_human_response(job_id: str) -> Optional[Dict[str, Any]]:
|
|
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]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def fetch_task_status(todo_id: str) -> Optional[str]:
|
|
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
|
+
)
|
|
228
|
+
|
|
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")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def fetch_all_agents() -> List[Dict[str, Any]]:
|
|
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
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def fetch_agent_data(user_ids: str) -> List[Dict[str, Any]]:
|
|
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
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def fetch_form_types(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]]]:
|
|
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": ""}])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def fetch_tenant_mcp_config(tenant_id: str) -> Optional[Dict[str, Any]]:
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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)
|
|
@@ -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)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from a2a.server.events import Event
|
|
9
|
+
from .logger import handle_error as _emit_error, log as _emit_log
|
|
10
|
+
from ..core.database import record_event, save_task_result
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _event_to_dict(event: Event) -> Dict[str, Any]:
|
|
14
|
+
try:
|
|
15
|
+
if hasattr(event, "__dict__"):
|
|
16
|
+
return {k: v for k, v in event.__dict__.items() if not k.startswith("_")}
|
|
17
|
+
return {"event": str(event)}
|
|
18
|
+
except Exception as e:
|
|
19
|
+
_emit_error("event dict 변환 실패", e, raise_error=False)
|
|
20
|
+
return {"event": str(event)}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def route_event(todo: Dict[str, Any], event: Event) -> None:
|
|
24
|
+
"""이벤트(dict)와 출력(output)을 구분해 처리.
|
|
25
|
+
|
|
26
|
+
- "event": CrewAI 등에서 발생한 실행 이벤트 → events 테이블 저장
|
|
27
|
+
- "output": 실행 결과 → save_task_result를 통해 중간/최종 여부에 따라 저장
|
|
28
|
+
"""
|
|
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)
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/logger.py
RENAMED
|
@@ -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 =
|
|
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,289 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import asyncio
|
|
4
|
-
import socket
|
|
5
|
-
import uuid
|
|
6
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
-
|
|
8
|
-
from dotenv import load_dotenv
|
|
9
|
-
from supabase import Client, create_client
|
|
10
|
-
import logging
|
|
11
|
-
import random
|
|
12
|
-
from typing import Callable, TypeVar
|
|
13
|
-
|
|
14
|
-
T = TypeVar("T")
|
|
15
|
-
|
|
16
|
-
from ..utils.logger import handle_error as _emit_error, log as _emit_log
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
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,
|
|
26
|
-
) -> 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
|
-
|
|
49
|
-
_supabase_client: Optional[Client] = None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
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)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def get_db_client() -> Client:
|
|
66
|
-
if _supabase_client is None:
|
|
67
|
-
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
68
|
-
return _supabase_client
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
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
|
-
|
|
80
|
-
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
|
-
|
|
94
|
-
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
|
-
|
|
109
|
-
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
|
-
|
|
129
|
-
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
|
-
|
|
142
|
-
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()
|
|
150
|
-
|
|
151
|
-
await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
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]
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
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
|
-
)
|
|
177
|
-
|
|
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")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
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": ""}])
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
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
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict
|
|
4
|
-
|
|
5
|
-
from a2a.server.events import Event
|
|
6
|
-
from .logger import handle_error as _emit_error, log as _emit_log
|
|
7
|
-
from ..core.database import record_event
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _event_to_dict(event: Event) -> Dict[str, Any]:
|
|
11
|
-
try:
|
|
12
|
-
if hasattr(event, "__dict__"):
|
|
13
|
-
return {k: v for k, v in event.__dict__.items() if not k.startswith("_")}
|
|
14
|
-
return {"event": str(event)}
|
|
15
|
-
except Exception as e:
|
|
16
|
-
_emit_error("event dict 변환 실패", e)
|
|
17
|
-
return {"event": str(event)}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def route_event(todo: Dict[str, Any], event: Event) -> None:
|
|
21
|
-
"""이벤트를 dict으로 변환해 events 테이블에 기록.
|
|
22
|
-
|
|
23
|
-
- 복잡한 라우팅/분기는 추후 확장. 현재는 단일 events 테이블에 기록.
|
|
24
|
-
- event_type은 None으로 두거나, 필요 시 상위에서 지정하도록 추후 확장 가능.
|
|
25
|
-
"""
|
|
26
|
-
data = _event_to_dict(event)
|
|
27
|
-
await record_event(todo, data, event_type=None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/core/__init__.py
RENAMED
|
File without changes
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/__init__.py
RENAMED
|
File without changes
|
{process_gpt_agent_sdk-0.1.2 → process_gpt_agent_sdk-0.1.3}/processgpt_agent_sdk/utils/summarizer.py
RENAMED
|
File without changes
|
|
File without changes
|