process-gpt-agent-sdk 0.3.16__py3-none-any.whl → 0.3.18__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.
- {process_gpt_agent_sdk-0.3.16.dist-info → process_gpt_agent_sdk-0.3.18.dist-info}/METADATA +7 -8
- process_gpt_agent_sdk-0.3.18.dist-info/RECORD +8 -0
- processgpt_agent_sdk/__init__.py +7 -15
- processgpt_agent_sdk/database.py +125 -397
- processgpt_agent_sdk/processgpt_agent_framework.py +320 -241
- processgpt_agent_sdk/utils.py +193 -0
- process_gpt_agent_sdk-0.3.16.dist-info/RECORD +0 -7
- {process_gpt_agent_sdk-0.3.16.dist-info → process_gpt_agent_sdk-0.3.18.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.3.16.dist-info → process_gpt_agent_sdk-0.3.18.dist-info}/top_level.txt +0 -0
processgpt_agent_sdk/database.py
CHANGED
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import asyncio
|
|
4
4
|
import socket
|
|
5
|
-
import uuid
|
|
6
5
|
from typing import Any, Dict, List, Optional, Tuple, Callable, TypeVar
|
|
7
6
|
|
|
8
7
|
from dotenv import load_dotenv
|
|
@@ -11,26 +10,22 @@ import logging
|
|
|
11
10
|
import random
|
|
12
11
|
|
|
13
12
|
T = TypeVar("T")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# 모듈 전역 로거 (정상 경로는 로깅하지 않고, 오류 시에만 사용)
|
|
17
13
|
logger = logging.getLogger(__name__)
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
# ============================================================================
|
|
21
|
-
# Utility: 재시도 헬퍼 및 유틸
|
|
22
|
-
# 설명: 동기 DB 호출을 안전하게 재시도 (지수 백오프 + 지터) 및 유틸
|
|
23
|
-
# ============================================================================
|
|
24
|
-
|
|
15
|
+
# ------------------------------ Retry & JSON utils ------------------------------
|
|
25
16
|
async def _async_retry(
|
|
26
|
-
fn: Callable[[],
|
|
17
|
+
fn: Callable[[], Any],
|
|
27
18
|
*,
|
|
28
19
|
name: str,
|
|
29
20
|
retries: int = 3,
|
|
30
21
|
base_delay: float = 0.8,
|
|
31
|
-
fallback: Optional[Callable[[],
|
|
32
|
-
) -> Optional[
|
|
33
|
-
"""
|
|
22
|
+
fallback: Optional[Callable[[], Any]] = None,
|
|
23
|
+
) -> Optional[Any]:
|
|
24
|
+
"""
|
|
25
|
+
- 각 시도 실패: warning 로깅(시도/지연/에러 포함)
|
|
26
|
+
- 최종 실패: FATAL 로깅(스택 포함), 예외는 재전파하지 않고 None 반환(기존 정책 유지)
|
|
27
|
+
- fallback 이 있으면 실행(실패 시에도 로깅 후 None)
|
|
28
|
+
"""
|
|
34
29
|
last_err: Optional[Exception] = None
|
|
35
30
|
for attempt in range(1, retries + 1):
|
|
36
31
|
try:
|
|
@@ -39,32 +34,29 @@ async def _async_retry(
|
|
|
39
34
|
last_err = e
|
|
40
35
|
jitter = random.uniform(0, 0.3)
|
|
41
36
|
delay = base_delay * (2 ** (attempt - 1)) + jitter
|
|
37
|
+
logger.warning(
|
|
38
|
+
"retry warn: name=%s attempt=%d/%d delay=%.2fs error=%s",
|
|
39
|
+
name, attempt, retries, delay, str(e),
|
|
40
|
+
exc_info=e
|
|
41
|
+
)
|
|
42
42
|
await asyncio.sleep(delay)
|
|
43
|
+
|
|
44
|
+
# 최종 실패
|
|
43
45
|
if last_err is not None:
|
|
44
46
|
logger.error(
|
|
45
|
-
"retry failed: name=%s retries=%s error=%s",
|
|
46
|
-
exc_info=last_err
|
|
47
|
+
"FATAL: retry failed: name=%s retries=%s error=%s",
|
|
48
|
+
name, retries, str(last_err), exc_info=last_err
|
|
47
49
|
)
|
|
50
|
+
|
|
48
51
|
if fallback is not None:
|
|
49
52
|
try:
|
|
50
|
-
|
|
51
|
-
return fb_val
|
|
53
|
+
return fallback()
|
|
52
54
|
except Exception as fb_err:
|
|
53
55
|
logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
|
|
54
56
|
return None
|
|
55
57
|
return None
|
|
56
|
-
|
|
57
58
|
|
|
58
|
-
def _is_valid_uuid(value: str) -> bool:
|
|
59
|
-
"""UUID 문자열 형식 검증 (v1~v8 포함)"""
|
|
60
|
-
try:
|
|
61
|
-
uuid.UUID(value)
|
|
62
|
-
return True
|
|
63
|
-
except Exception:
|
|
64
|
-
return False
|
|
65
|
-
|
|
66
59
|
def _to_jsonable(value: Any) -> Any:
|
|
67
|
-
"""간단한 JSON 변환: dict 재귀, list/tuple/set→list, 기본형 유지, 나머지는 repr."""
|
|
68
60
|
try:
|
|
69
61
|
if value is None or isinstance(value, (str, int, float, bool)):
|
|
70
62
|
return value
|
|
@@ -78,16 +70,10 @@ def _to_jsonable(value: Any) -> Any:
|
|
|
78
70
|
except Exception:
|
|
79
71
|
return repr(value)
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
# ============================================================================
|
|
83
|
-
# DB 연결/클라이언트
|
|
84
|
-
# 설명: 환경 변수 로드, Supabase 클라이언트 초기화/반환, 컨슈머 식별자
|
|
85
|
-
# ============================================================================
|
|
73
|
+
# ------------------------------ DB Client ------------------------------
|
|
86
74
|
_supabase_client: Optional[Client] = None
|
|
87
75
|
|
|
88
|
-
|
|
89
76
|
def initialize_db() -> None:
|
|
90
|
-
"""환경변수 로드 및 Supabase 클라이언트 초기화"""
|
|
91
77
|
global _supabase_client
|
|
92
78
|
if _supabase_client is not None:
|
|
93
79
|
return
|
|
@@ -103,16 +89,12 @@ def initialize_db() -> None:
|
|
|
103
89
|
logger.error("initialize_db failed: %s", str(e), exc_info=e)
|
|
104
90
|
raise
|
|
105
91
|
|
|
106
|
-
|
|
107
92
|
def get_db_client() -> Client:
|
|
108
|
-
"""초기화된 Supabase 클라이언트 반환."""
|
|
109
93
|
if _supabase_client is None:
|
|
110
94
|
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
111
95
|
return _supabase_client
|
|
112
96
|
|
|
113
|
-
|
|
114
97
|
def get_consumer_id() -> str:
|
|
115
|
-
"""파드/프로세스 식별자 생성(CONSUMER_ID>HOST:PID)."""
|
|
116
98
|
env_consumer = os.getenv("CONSUMER_ID")
|
|
117
99
|
if env_consumer:
|
|
118
100
|
return env_consumer
|
|
@@ -120,17 +102,9 @@ def get_consumer_id() -> str:
|
|
|
120
102
|
pid = os.getpid()
|
|
121
103
|
return f"{host}:{pid}"
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
# ============================================================================
|
|
125
|
-
# 데이터 조회
|
|
126
|
-
# 설명: TODOLIST 테이블 조회, 완료 output 목록 조회, 이벤트 조회, 폼 조회, 테넌트 MCP 설정 조회, 사용자 및 에이전트 조회
|
|
127
|
-
# ============================================================================
|
|
105
|
+
# ------------------------------ Polling ------------------------------
|
|
128
106
|
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
- 정상 동작 시 로그를 남기지 않는다.
|
|
132
|
-
- 예외 시에만 풍부한 에러 정보를 남기되, 호출자에게 None을 반환하여 폴링 루프가 중단되지 않게 한다.
|
|
133
|
-
"""
|
|
107
|
+
"""단일 RPC(fetch_pending_task) 호출: p_env 로 dev/prod 분기"""
|
|
134
108
|
if agent_orch is None:
|
|
135
109
|
agent_orch = ""
|
|
136
110
|
if consumer is None:
|
|
@@ -139,319 +113,126 @@ async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict
|
|
|
139
113
|
def _call():
|
|
140
114
|
client = get_db_client()
|
|
141
115
|
consumer_id = consumer or socket.gethostname()
|
|
142
|
-
env = (os.getenv("ENV") or "").lower()
|
|
143
|
-
|
|
144
|
-
if env == "dev":
|
|
145
|
-
resp = client.rpc(
|
|
146
|
-
"fetch_pending_task_dev",
|
|
147
|
-
{"p_agent_orch": agent_orch, "p_consumer": consumer_id, "p_limit": 1, "p_tenant_id": "uengine"},
|
|
148
|
-
).execute()
|
|
149
|
-
else:
|
|
150
|
-
resp = client.rpc(
|
|
151
|
-
"fetch_pending_task",
|
|
152
|
-
{"p_agent_orch": agent_orch, "p_consumer": consumer_id, "p_limit": 1},
|
|
153
|
-
).execute()
|
|
154
|
-
|
|
155
|
-
rows = resp.data or []
|
|
156
|
-
return rows[0] if rows else None
|
|
157
|
-
|
|
158
|
-
return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
|
|
159
116
|
|
|
117
|
+
# ENV 값을 dev / (그외=prod) 로만 정규화
|
|
118
|
+
p_env = (os.getenv("ENV") or "").lower()
|
|
119
|
+
if p_env != "dev":
|
|
120
|
+
p_env = "prod"
|
|
121
|
+
|
|
122
|
+
resp = client.rpc(
|
|
123
|
+
"fetch_pending_task",
|
|
124
|
+
{
|
|
125
|
+
"p_agent_orch": agent_orch,
|
|
126
|
+
"p_consumer": consumer_id,
|
|
127
|
+
"p_limit": 1,
|
|
128
|
+
"p_env": p_env,
|
|
129
|
+
},
|
|
130
|
+
).execute()
|
|
160
131
|
|
|
161
|
-
def fetch_human_response_sync(job_id: str) -> Optional[Dict[str, Any]]:
|
|
162
|
-
"""events에서 특정 job_id의 human_response 조회"""
|
|
163
|
-
if not job_id:
|
|
164
|
-
return None
|
|
165
|
-
try:
|
|
166
|
-
client = get_db_client()
|
|
167
|
-
resp = (
|
|
168
|
-
client
|
|
169
|
-
.table("events")
|
|
170
|
-
.select("*")
|
|
171
|
-
.eq("job_id", job_id)
|
|
172
|
-
.eq("event_type", "human_response")
|
|
173
|
-
.execute()
|
|
174
|
-
)
|
|
175
132
|
rows = resp.data or []
|
|
176
133
|
return rows[0] if rows else None
|
|
177
|
-
except Exception as e:
|
|
178
|
-
logger.error("fetch_human_response_sync failed: %s", str(e), exc_info=e)
|
|
179
|
-
return None
|
|
180
134
|
|
|
135
|
+
return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
|
|
181
136
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
client.table("todolist").select("draft_status").eq("id", todo_id).single().execute()
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
resp = await _async_retry(_call, name="fetch_task_status")
|
|
194
|
-
except Exception as e:
|
|
195
|
-
logger.error("fetch_task_status fatal: %s", str(e), exc_info=e)
|
|
196
|
-
return None
|
|
197
|
-
if not resp or not getattr(resp, "data", None):
|
|
198
|
-
return None
|
|
199
|
-
try:
|
|
200
|
-
return resp.data.get("draft_status")
|
|
201
|
-
except Exception as e:
|
|
202
|
-
logger.error("fetch_task_status parse error: %s", str(e), exc_info=e)
|
|
203
|
-
return None
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
async def fetch_all_agents() -> List[Dict[str, Any]]:
|
|
208
|
-
"""모든 에이전트 목록을 정규화하여 반환한다."""
|
|
209
|
-
def _call():
|
|
210
|
-
client = get_db_client()
|
|
211
|
-
return (
|
|
212
|
-
client.table("users")
|
|
213
|
-
.select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent, endpoint")
|
|
214
|
-
.eq("is_agent", True)
|
|
215
|
-
.execute()
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
resp = await _async_retry(_call, name="fetch_all_agents")
|
|
220
|
-
except Exception as e:
|
|
221
|
-
logger.error("fetch_all_agents fatal: %s", str(e), exc_info=e)
|
|
222
|
-
return []
|
|
223
|
-
rows = resp.data or [] if resp else []
|
|
224
|
-
try:
|
|
225
|
-
normalized: List[Dict[str, Any]] = []
|
|
226
|
-
for row in rows:
|
|
227
|
-
normalized.append(
|
|
228
|
-
{
|
|
229
|
-
"id": row.get("id"),
|
|
230
|
-
"name": row.get("username"),
|
|
231
|
-
"role": row.get("role"),
|
|
232
|
-
"goal": row.get("goal"),
|
|
233
|
-
"persona": row.get("persona"),
|
|
234
|
-
"tools": row.get("tools") or "mem0",
|
|
235
|
-
"profile": row.get("profile"),
|
|
236
|
-
"model": row.get("model"),
|
|
237
|
-
"tenant_id": row.get("tenant_id"),
|
|
238
|
-
"endpoint": row.get("endpoint"),
|
|
239
|
-
}
|
|
240
|
-
)
|
|
241
|
-
return normalized
|
|
242
|
-
except Exception as e:
|
|
243
|
-
logger.error("fetch_all_agents parse error: %s", str(e), exc_info=e)
|
|
244
|
-
return []
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
async def fetch_agent_data(user_ids: str) -> List[Dict[str, Any]]:
|
|
248
|
-
"""TODOLIST의 user_id 값으로, 역할로 지정된 에이전트를 조회하고 정규화해 반환한다."""
|
|
249
|
-
|
|
250
|
-
raw_ids = [x.strip() for x in (user_ids or "").split(",") if x.strip()]
|
|
251
|
-
valid_ids = [x for x in raw_ids if _is_valid_uuid(x)]
|
|
252
|
-
|
|
253
|
-
if not valid_ids:
|
|
254
|
-
return await fetch_all_agents()
|
|
255
|
-
|
|
137
|
+
# ------------------------------ Context Bundle ------------------------------
|
|
138
|
+
async def fetch_context_bundle(
|
|
139
|
+
proc_inst_id: str,
|
|
140
|
+
tenant_id: str,
|
|
141
|
+
tool_val: str,
|
|
142
|
+
user_ids: str,
|
|
143
|
+
) -> Tuple[str, Optional[Dict[str, Any]], Tuple[Optional[str], List[Dict[str, Any]], Optional[str]], List[Dict[str, Any]]]:
|
|
256
144
|
def _call():
|
|
257
145
|
client = get_db_client()
|
|
258
|
-
resp = (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
146
|
+
resp = client.rpc(
|
|
147
|
+
"fetch_context_bundle",
|
|
148
|
+
{
|
|
149
|
+
"p_proc_inst_id": proc_inst_id or "",
|
|
150
|
+
"p_tenant_id": tenant_id or "",
|
|
151
|
+
"p_tool": tool_val or "",
|
|
152
|
+
"p_user_ids": user_ids or "",
|
|
153
|
+
},
|
|
154
|
+
).execute()
|
|
266
155
|
rows = resp.data or []
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
)
|
|
283
|
-
return normalized
|
|
156
|
+
row = rows[0] if rows else {}
|
|
157
|
+
notify = (row.get("notify_emails") or "").strip()
|
|
158
|
+
mcp = row.get("tenant_mcp") or None
|
|
159
|
+
form_id = row.get("form_id")
|
|
160
|
+
form_fields = row.get("form_fields")
|
|
161
|
+
form_html = row.get("form_html")
|
|
162
|
+
|
|
163
|
+
# form 정보가 없는 경우 자유형식 폼으로 처리
|
|
164
|
+
if not form_id or not form_fields:
|
|
165
|
+
form_id = "freeform"
|
|
166
|
+
form_fields = [{"key": "freeform", "type": "textarea", "text": "자유형식 입력", "placeholder": "원하는 내용을 자유롭게 입력해주세요."}]
|
|
167
|
+
form_html = None
|
|
168
|
+
agents = row.get("agents") or []
|
|
169
|
+
return notify, mcp, (form_id, form_fields, form_html), agents
|
|
284
170
|
|
|
285
171
|
try:
|
|
286
|
-
|
|
172
|
+
return await _async_retry(_call, name="fetch_context_bundle", fallback=lambda: ("", None, (None, [], None), []))
|
|
287
173
|
except Exception as e:
|
|
288
|
-
logger.error("
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
if not result:
|
|
292
|
-
return await fetch_all_agents()
|
|
293
|
-
|
|
294
|
-
return result
|
|
174
|
+
logger.error("fetch_context_bundle fatal: %s", str(e), exc_info=e)
|
|
175
|
+
return ("", None, (None, [], None), [])
|
|
295
176
|
|
|
177
|
+
# ------------------------------ Events & Results ------------------------------
|
|
178
|
+
async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
|
|
179
|
+
"""
|
|
180
|
+
이벤트 다건 저장:
|
|
181
|
+
- 성공: 'record_events_bulk ok'
|
|
182
|
+
- 실패(최종): '❌ record_events_bulk failed' (개수 포함)
|
|
183
|
+
"""
|
|
184
|
+
if not payloads:
|
|
185
|
+
return
|
|
296
186
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
form_id = tool_val[12:] if tool_val.startswith("formHandler:") else tool_val
|
|
304
|
-
|
|
305
|
-
def _call():
|
|
306
|
-
client = get_db_client()
|
|
307
|
-
resp = (
|
|
308
|
-
client
|
|
309
|
-
.table("form_def")
|
|
310
|
-
.select("fields_json, html")
|
|
311
|
-
.eq("id", form_id)
|
|
312
|
-
.eq("tenant_id", tenant_id)
|
|
313
|
-
.execute()
|
|
314
|
-
)
|
|
315
|
-
fields_json = resp.data[0].get("fields_json") if resp.data else None
|
|
316
|
-
form_html = resp.data[0].get("html") if resp.data else None
|
|
317
|
-
if not fields_json:
|
|
318
|
-
return form_id, [{"key": form_id, "type": "default", "text": ""}], form_html
|
|
319
|
-
return form_id, fields_json, form_html
|
|
320
|
-
|
|
321
|
-
try:
|
|
322
|
-
resp = await _async_retry(
|
|
323
|
-
_call,
|
|
324
|
-
name="fetch_form_types",
|
|
325
|
-
fallback=lambda: (form_id, [{"key": form_id, "type": "default", "text": ""}], None),
|
|
326
|
-
)
|
|
327
|
-
except Exception as e:
|
|
328
|
-
logger.error("fetch_form_types fatal: %s", str(e), exc_info=e)
|
|
329
|
-
resp = None
|
|
330
|
-
return resp if resp else (form_id, [{"key": form_id, "type": "default", "text": ""}], None)
|
|
331
|
-
|
|
187
|
+
safe_list: List[Dict[str, Any]] = []
|
|
188
|
+
for p in payloads:
|
|
189
|
+
sp = _to_jsonable(p)
|
|
190
|
+
if isinstance(sp, dict) and sp.get("status", "") == "":
|
|
191
|
+
sp["status"] = None
|
|
192
|
+
safe_list.append(sp)
|
|
332
193
|
|
|
333
|
-
async def fetch_tenant_mcp_config(tenant_id: str) -> Optional[Dict[str, Any]]:
|
|
334
|
-
"""테넌트 MCP 설정을 조회해 반환한다."""
|
|
335
|
-
if not tenant_id:
|
|
336
|
-
return None
|
|
337
194
|
def _call():
|
|
338
195
|
client = get_db_client()
|
|
339
|
-
return client.
|
|
340
|
-
|
|
341
|
-
try:
|
|
342
|
-
resp = await _async_retry(_call, name="fetch_tenant_mcp_config", fallback=lambda: None)
|
|
343
|
-
except Exception as e:
|
|
344
|
-
logger.error("fetch_tenant_mcp_config fatal: %s", str(e), exc_info=e)
|
|
345
|
-
return None
|
|
346
|
-
return resp.data.get("mcp") if resp and getattr(resp, "data", None) else None
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
async def fetch_human_users_by_proc_inst_id(proc_inst_id: str) -> str:
|
|
350
|
-
"""proc_inst_id로 현재 프로세스의 모든 사용자 이메일 목록을 쉼표로 반환한다."""
|
|
351
|
-
if not proc_inst_id:
|
|
352
|
-
return ""
|
|
353
|
-
|
|
354
|
-
def _sync():
|
|
355
|
-
try:
|
|
356
|
-
supabase = get_db_client()
|
|
357
|
-
|
|
358
|
-
resp = (
|
|
359
|
-
supabase
|
|
360
|
-
.table('todolist')
|
|
361
|
-
.select('user_id')
|
|
362
|
-
.eq('proc_inst_id', proc_inst_id)
|
|
363
|
-
.execute()
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
if not resp.data:
|
|
367
|
-
return ""
|
|
368
|
-
|
|
369
|
-
all_user_ids = set()
|
|
370
|
-
for row in resp.data:
|
|
371
|
-
user_id = row.get('user_id', '')
|
|
372
|
-
if user_id:
|
|
373
|
-
ids = [id.strip() for id in user_id.split(',') if id.strip()]
|
|
374
|
-
all_user_ids.update(ids)
|
|
375
|
-
|
|
376
|
-
if not all_user_ids:
|
|
377
|
-
return ""
|
|
378
|
-
|
|
379
|
-
human_user_emails = []
|
|
380
|
-
for user_id in all_user_ids:
|
|
381
|
-
if not _is_valid_uuid(user_id):
|
|
382
|
-
continue
|
|
383
|
-
|
|
384
|
-
user_resp = (
|
|
385
|
-
supabase
|
|
386
|
-
.table('users')
|
|
387
|
-
.select('id, email, is_agent')
|
|
388
|
-
.eq('id', user_id)
|
|
389
|
-
.execute()
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
if user_resp.data:
|
|
393
|
-
user = user_resp.data[0]
|
|
394
|
-
is_agent = user.get('is_agent')
|
|
395
|
-
if not is_agent:
|
|
396
|
-
email = (user.get('email') or '').strip()
|
|
397
|
-
if email:
|
|
398
|
-
human_user_emails.append(email)
|
|
399
|
-
|
|
400
|
-
return ','.join(human_user_emails)
|
|
401
|
-
|
|
402
|
-
except Exception as e:
|
|
403
|
-
logger.error("fetch_human_users_by_proc_inst_id failed: %s", str(e), exc_info=e)
|
|
404
|
-
return ""
|
|
405
|
-
|
|
406
|
-
return await asyncio.to_thread(_sync)
|
|
196
|
+
return client.rpc("record_events_bulk", {"p_events": safe_list}).execute()
|
|
407
197
|
|
|
198
|
+
res = await _async_retry(_call, name="record_events_bulk", fallback=lambda: None)
|
|
199
|
+
if res is None:
|
|
200
|
+
logger.error("❌ record_events_bulk failed: events not persisted count=%d", len(safe_list))
|
|
201
|
+
else:
|
|
202
|
+
logger.info("record_events_bulk ok: count=%d", len(safe_list))
|
|
408
203
|
|
|
409
|
-
# ============================================================================
|
|
410
|
-
# 데이터 저장
|
|
411
|
-
# 설명: 이벤트/알림/작업 결과 저장
|
|
412
|
-
# ============================================================================
|
|
413
204
|
async def record_event(payload: Dict[str, Any]) -> None:
|
|
414
|
-
"""
|
|
415
|
-
|
|
416
|
-
|
|
205
|
+
"""
|
|
206
|
+
단건 이벤트 저장.
|
|
207
|
+
- 성공: 'record_event ok'
|
|
208
|
+
- 실패(최종): '❌ record_event failed'
|
|
209
|
+
- 실패해도 워크플로우는 계속(요청사항)
|
|
210
|
+
"""
|
|
211
|
+
if not payload:
|
|
417
212
|
return
|
|
213
|
+
|
|
418
214
|
def _call():
|
|
419
215
|
client = get_db_client()
|
|
420
216
|
safe_payload = _to_jsonable(payload)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
status_val = safe_payload.get("status")
|
|
424
|
-
if status_val == "":
|
|
425
|
-
safe_payload["status"] = None
|
|
217
|
+
if isinstance(safe_payload, dict) and safe_payload.get("status", "") == "":
|
|
218
|
+
safe_payload["status"] = None
|
|
426
219
|
return client.table("events").insert(safe_payload).execute()
|
|
427
220
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
except Exception:
|
|
434
|
-
logger.error("record_event fatal (payload dump failed): %s", str(e), exc_info=e)
|
|
435
|
-
return
|
|
436
|
-
if resp is None:
|
|
437
|
-
try:
|
|
438
|
-
logger.error("events insert 실패: payload=%s", json.dumps(_to_jsonable(payload), ensure_ascii=False))
|
|
439
|
-
except Exception:
|
|
440
|
-
logger.error("events insert 실패 (payload dump failed)")
|
|
441
|
-
|
|
442
|
-
|
|
221
|
+
res = await _async_retry(_call, name="record_event", fallback=lambda: None)
|
|
222
|
+
if res is None:
|
|
223
|
+
logger.error("❌ record_event failed =%s", payload.get("event_type"))
|
|
224
|
+
else:
|
|
225
|
+
logger.info("record_event ok: event_type=%s", payload.get("event_type"))
|
|
443
226
|
|
|
444
227
|
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
445
|
-
"""작업 결과를 저장한다. final=True 시 최종 저장."""
|
|
446
228
|
if not todo_id:
|
|
447
229
|
logger.error("save_task_result invalid todo_id: %s", str(todo_id))
|
|
448
230
|
return
|
|
449
|
-
|
|
450
|
-
def
|
|
231
|
+
|
|
232
|
+
def _safe(val: Any) -> Any:
|
|
451
233
|
try:
|
|
452
234
|
return _to_jsonable(val)
|
|
453
|
-
except Exception
|
|
454
|
-
logger.error("save_task_result payload serialization failed: %s", str(e), exc_info=e)
|
|
235
|
+
except Exception:
|
|
455
236
|
try:
|
|
456
237
|
return {"repr": repr(val)}
|
|
457
238
|
except Exception:
|
|
@@ -459,79 +240,26 @@ async def save_task_result(todo_id: str, result: Any, final: bool = False) -> No
|
|
|
459
240
|
|
|
460
241
|
def _call():
|
|
461
242
|
client = get_db_client()
|
|
462
|
-
payload =
|
|
463
|
-
return client.rpc(
|
|
464
|
-
"save_task_result",
|
|
465
|
-
{"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)},
|
|
466
|
-
).execute()
|
|
467
|
-
|
|
468
|
-
try:
|
|
469
|
-
await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
470
|
-
except Exception as e:
|
|
471
|
-
logger.error("save_task_result fatal: %s", str(e), exc_info=e)
|
|
243
|
+
payload = _safe(result)
|
|
244
|
+
return client.rpc("save_task_result", {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)}).execute()
|
|
472
245
|
|
|
246
|
+
res = await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
247
|
+
if res is None:
|
|
248
|
+
logger.error("❌ save_task_result failed todo_id=%s", todo_id)
|
|
249
|
+
else:
|
|
250
|
+
logger.info("save_task_result ok todo_id=%s", todo_id)
|
|
473
251
|
|
|
474
|
-
|
|
475
|
-
*,
|
|
476
|
-
title: str,
|
|
477
|
-
notif_type: str,
|
|
478
|
-
description: Optional[str] = None,
|
|
479
|
-
user_ids_csv: Optional[str] = None,
|
|
480
|
-
tenant_id: Optional[str] = None,
|
|
481
|
-
url: Optional[str] = None,
|
|
482
|
-
from_user_id: Optional[str] = None,
|
|
483
|
-
) -> None:
|
|
484
|
-
"""notifications 테이블에 알림 저장"""
|
|
485
|
-
try:
|
|
486
|
-
# 대상 사용자가 없으면 작업 생략
|
|
487
|
-
if not user_ids_csv:
|
|
488
|
-
return
|
|
489
|
-
|
|
490
|
-
client = get_db_client()
|
|
491
|
-
|
|
492
|
-
user_ids: List[str] = [uid.strip() for uid in user_ids_csv.split(',') if uid and uid.strip()]
|
|
493
|
-
if not user_ids:
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
rows: List[Dict[str, Any]] = []
|
|
497
|
-
for uid in user_ids:
|
|
498
|
-
rows.append(
|
|
499
|
-
{
|
|
500
|
-
"id": str(uuid.uuid4()),
|
|
501
|
-
"user_id": uid,
|
|
502
|
-
"tenant_id": tenant_id,
|
|
503
|
-
"title": title,
|
|
504
|
-
"description": description,
|
|
505
|
-
"type": notif_type,
|
|
506
|
-
"url": url,
|
|
507
|
-
"from_user_id": from_user_id,
|
|
508
|
-
}
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
client.table("notifications").insert(rows).execute()
|
|
512
|
-
except Exception as e:
|
|
513
|
-
logger.error("save_notification failed: %s", str(e), exc_info=e)
|
|
514
|
-
|
|
515
|
-
# ============================================================================
|
|
516
|
-
# 상태 변경
|
|
517
|
-
# 설명: 실패 작업 상태 업데이트
|
|
518
|
-
# ============================================================================
|
|
519
|
-
|
|
252
|
+
# ------------------------------ Failure Status ------------------------------
|
|
520
253
|
async def update_task_error(todo_id: str) -> None:
|
|
521
|
-
"""실패 작업의 상태를 FAILED로 갱신한다."""
|
|
522
254
|
if not todo_id:
|
|
523
255
|
return
|
|
256
|
+
|
|
524
257
|
def _call():
|
|
525
258
|
client = get_db_client()
|
|
526
|
-
return (
|
|
527
|
-
client
|
|
528
|
-
.table('todolist')
|
|
529
|
-
.update({'draft_status': 'FAILED', 'consumer': None})
|
|
530
|
-
.eq('id', todo_id)
|
|
531
|
-
.execute()
|
|
532
|
-
)
|
|
259
|
+
return client.table("todolist").update({"draft_status": "FAILED", "consumer": None}).eq("id", todo_id).execute()
|
|
533
260
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
261
|
+
res = await _async_retry(_call, name="update_task_error", fallback=lambda: None)
|
|
262
|
+
if res is None:
|
|
263
|
+
logger.error("❌ update_task_error failed todo_id=%s", todo_id)
|
|
264
|
+
else:
|
|
265
|
+
logger.info("update_task_error ok todo_id=%s", todo_id)
|