process-gpt-agent-sdk 0.3.15__py3-none-any.whl → 0.3.17__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.15.dist-info → process_gpt_agent_sdk-0.3.17.dist-info}/METADATA +7 -8
- process_gpt_agent_sdk-0.3.17.dist-info/RECORD +8 -0
- processgpt_agent_sdk/__init__.py +25 -33
- processgpt_agent_sdk/database.py +119 -397
- processgpt_agent_sdk/processgpt_agent_framework.py +307 -240
- processgpt_agent_sdk/utils.py +100 -0
- process_gpt_agent_sdk-0.3.15.dist-info/RECORD +0 -7
- {process_gpt_agent_sdk-0.3.15.dist-info → process_gpt_agent_sdk-0.3.17.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.3.15.dist-info → process_gpt_agent_sdk-0.3.17.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,120 @@ 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
|
-
|
|
181
134
|
|
|
182
|
-
|
|
183
|
-
"""todo의 draft_status를 조회한다."""
|
|
184
|
-
if not todo_id:
|
|
185
|
-
return None
|
|
186
|
-
def _call():
|
|
187
|
-
client = get_db_client()
|
|
188
|
-
return (
|
|
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()
|
|
135
|
+
return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
|
|
255
136
|
|
|
137
|
+
# ------------------------------ Context Bundle ------------------------------
|
|
138
|
+
async def fetch_context_bundle(
|
|
139
|
+
proc_inst_id: str,
|
|
140
|
+
tenant_id: str,
|
|
141
|
+
tool_val: str,
|
|
142
|
+
user_ids: str,
|
|
143
|
+
) -> Tuple[str, Optional[Dict[str, Any]], Tuple[Optional[str], List[Dict[str, Any]], Optional[str]], List[Dict[str, Any]]]:
|
|
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
|
-
"persona": row.get("persona"),
|
|
276
|
-
"tools": row.get("tools") or "mem0",
|
|
277
|
-
"profile": row.get("profile"),
|
|
278
|
-
"model": row.get("model"),
|
|
279
|
-
"tenant_id": row.get("tenant_id"),
|
|
280
|
-
"endpoint": row.get("endpoint"),
|
|
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") or [{"key": form_id, "type": "default", "text": ""}]
|
|
161
|
+
form_html = row.get("form_html")
|
|
162
|
+
agents = row.get("agents") or []
|
|
163
|
+
return notify, mcp, (form_id, form_fields, form_html), agents
|
|
284
164
|
|
|
285
165
|
try:
|
|
286
|
-
|
|
166
|
+
return await _async_retry(_call, name="fetch_context_bundle", fallback=lambda: ("", None, (None, [], None), []))
|
|
287
167
|
except Exception as e:
|
|
288
|
-
logger.error("
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
if not result:
|
|
292
|
-
return await fetch_all_agents()
|
|
293
|
-
|
|
294
|
-
return result
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
async def fetch_form_types(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
|
|
298
|
-
"""폼 타입 정의를 조회해 (form_id, fields, html)로 반환한다."""
|
|
299
|
-
if tool_val is None:
|
|
300
|
-
tool_val = ""
|
|
301
|
-
if tenant_id is None:
|
|
302
|
-
tenant_id = ""
|
|
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
|
|
168
|
+
logger.error("fetch_context_bundle fatal: %s", str(e), exc_info=e)
|
|
169
|
+
return ("", None, (None, [], None), [])
|
|
320
170
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return resp if resp else (form_id, [{"key": form_id, "type": "default", "text": ""}], None)
|
|
171
|
+
# ------------------------------ Events & Results ------------------------------
|
|
172
|
+
async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
|
|
173
|
+
"""
|
|
174
|
+
이벤트 다건 저장:
|
|
175
|
+
- 성공: 'record_events_bulk ok'
|
|
176
|
+
- 실패(최종): '❌ record_events_bulk failed' (개수 포함)
|
|
177
|
+
"""
|
|
178
|
+
if not payloads:
|
|
179
|
+
return
|
|
331
180
|
|
|
181
|
+
safe_list: List[Dict[str, Any]] = []
|
|
182
|
+
for p in payloads:
|
|
183
|
+
sp = _to_jsonable(p)
|
|
184
|
+
if isinstance(sp, dict) and sp.get("status", "") == "":
|
|
185
|
+
sp["status"] = None
|
|
186
|
+
safe_list.append(sp)
|
|
332
187
|
|
|
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
188
|
def _call():
|
|
338
189
|
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)
|
|
190
|
+
return client.rpc("record_events_bulk", {"p_events": safe_list}).execute()
|
|
407
191
|
|
|
192
|
+
res = await _async_retry(_call, name="record_events_bulk", fallback=lambda: None)
|
|
193
|
+
if res is None:
|
|
194
|
+
logger.error("❌ record_events_bulk failed: events not persisted count=%d", len(safe_list))
|
|
195
|
+
else:
|
|
196
|
+
logger.info("record_events_bulk ok: count=%d", len(safe_list))
|
|
408
197
|
|
|
409
|
-
# ============================================================================
|
|
410
|
-
# 데이터 저장
|
|
411
|
-
# 설명: 이벤트/알림/작업 결과 저장
|
|
412
|
-
# ============================================================================
|
|
413
198
|
async def record_event(payload: Dict[str, Any]) -> None:
|
|
414
|
-
"""
|
|
415
|
-
|
|
416
|
-
|
|
199
|
+
"""
|
|
200
|
+
단건 이벤트 저장.
|
|
201
|
+
- 성공: 'record_event ok'
|
|
202
|
+
- 실패(최종): '❌ record_event failed'
|
|
203
|
+
- 실패해도 워크플로우는 계속(요청사항)
|
|
204
|
+
"""
|
|
205
|
+
if not payload:
|
|
417
206
|
return
|
|
207
|
+
|
|
418
208
|
def _call():
|
|
419
209
|
client = get_db_client()
|
|
420
210
|
safe_payload = _to_jsonable(payload)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
status_val = safe_payload.get("status")
|
|
424
|
-
if status_val == "":
|
|
425
|
-
safe_payload["status"] = None
|
|
211
|
+
if isinstance(safe_payload, dict) and safe_payload.get("status", "") == "":
|
|
212
|
+
safe_payload["status"] = None
|
|
426
213
|
return client.table("events").insert(safe_payload).execute()
|
|
427
214
|
|
|
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
|
-
|
|
215
|
+
res = await _async_retry(_call, name="record_event", fallback=lambda: None)
|
|
216
|
+
if res is None:
|
|
217
|
+
logger.error("❌ record_event failed =%s", payload.get("event_type"))
|
|
218
|
+
else:
|
|
219
|
+
logger.info("record_event ok: event_type=%s", payload.get("event_type"))
|
|
443
220
|
|
|
444
221
|
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
445
|
-
"""작업 결과를 저장한다. final=True 시 최종 저장."""
|
|
446
222
|
if not todo_id:
|
|
447
223
|
logger.error("save_task_result invalid todo_id: %s", str(todo_id))
|
|
448
224
|
return
|
|
449
|
-
|
|
450
|
-
def
|
|
225
|
+
|
|
226
|
+
def _safe(val: Any) -> Any:
|
|
451
227
|
try:
|
|
452
228
|
return _to_jsonable(val)
|
|
453
|
-
except Exception
|
|
454
|
-
logger.error("save_task_result payload serialization failed: %s", str(e), exc_info=e)
|
|
229
|
+
except Exception:
|
|
455
230
|
try:
|
|
456
231
|
return {"repr": repr(val)}
|
|
457
232
|
except Exception:
|
|
@@ -459,79 +234,26 @@ async def save_task_result(todo_id: str, result: Any, final: bool = False) -> No
|
|
|
459
234
|
|
|
460
235
|
def _call():
|
|
461
236
|
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)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
def save_notification(
|
|
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)
|
|
237
|
+
payload = _safe(result)
|
|
238
|
+
return client.rpc("save_task_result", {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)}).execute()
|
|
514
239
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
240
|
+
res = await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
241
|
+
if res is None:
|
|
242
|
+
logger.error("❌ save_task_result failed todo_id=%s", todo_id)
|
|
243
|
+
else:
|
|
244
|
+
logger.info("save_task_result ok todo_id=%s", todo_id)
|
|
519
245
|
|
|
246
|
+
# ------------------------------ Failure Status ------------------------------
|
|
520
247
|
async def update_task_error(todo_id: str) -> None:
|
|
521
|
-
"""실패 작업의 상태를 FAILED로 갱신한다."""
|
|
522
248
|
if not todo_id:
|
|
523
249
|
return
|
|
250
|
+
|
|
524
251
|
def _call():
|
|
525
252
|
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
|
-
)
|
|
253
|
+
return client.table("todolist").update({"draft_status": "FAILED", "consumer": None}).eq("id", todo_id).execute()
|
|
533
254
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
255
|
+
res = await _async_retry(_call, name="update_task_error", fallback=lambda: None)
|
|
256
|
+
if res is None:
|
|
257
|
+
logger.error("❌ update_task_error failed todo_id=%s", todo_id)
|
|
258
|
+
else:
|
|
259
|
+
logger.info("update_task_error ok todo_id=%s", todo_id)
|