process-gpt-agent-sdk 0.4.4__py3-none-any.whl → 0.4.6__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.4.4.dist-info → process_gpt_agent_sdk-0.4.6.dist-info}/METADATA +1 -1
- process_gpt_agent_sdk-0.4.6.dist-info/RECORD +9 -0
- processgpt_agent_sdk/__init__.py +44 -44
- processgpt_agent_sdk/database.py +441 -428
- processgpt_agent_sdk/processgpt_agent_framework.py +497 -494
- processgpt_agent_sdk/utils.py +208 -208
- process_gpt_agent_sdk-0.4.4.dist-info/RECORD +0 -9
- {process_gpt_agent_sdk-0.4.4.dist-info → process_gpt_agent_sdk-0.4.6.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.4.4.dist-info → process_gpt_agent_sdk-0.4.6.dist-info}/top_level.txt +0 -0
processgpt_agent_sdk/database.py
CHANGED
|
@@ -1,429 +1,442 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import asyncio
|
|
4
|
-
import socket
|
|
5
|
-
from typing import Any, Dict, List, Optional, Tuple, Callable, TypeVar
|
|
6
|
-
|
|
7
|
-
from dotenv import load_dotenv
|
|
8
|
-
from supabase import Client, create_client
|
|
9
|
-
import logging
|
|
10
|
-
import random
|
|
11
|
-
|
|
12
|
-
T = TypeVar("T")
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
# ------------------------------ Retry & JSON utils ------------------------------
|
|
16
|
-
async def _async_retry(
|
|
17
|
-
fn: Callable[[], Any],
|
|
18
|
-
*,
|
|
19
|
-
name: str,
|
|
20
|
-
retries: int = 3,
|
|
21
|
-
base_delay: float = 0.8,
|
|
22
|
-
fallback: Optional[Callable[[], Any]] = None,
|
|
23
|
-
) -> Optional[Any]:
|
|
24
|
-
"""
|
|
25
|
-
- 각 시도 실패: warning 로깅(시도/지연/에러 포함)
|
|
26
|
-
- 최종 실패: FATAL 로깅(스택 포함), 예외는 재전파하지 않고 None 반환(기존 정책 유지)
|
|
27
|
-
- fallback 이 있으면 실행(실패 시에도 로깅 후 None)
|
|
28
|
-
"""
|
|
29
|
-
last_err: Optional[Exception] = None
|
|
30
|
-
for attempt in range(1, retries + 1):
|
|
31
|
-
try:
|
|
32
|
-
return await asyncio.to_thread(fn)
|
|
33
|
-
except Exception as e:
|
|
34
|
-
last_err = e
|
|
35
|
-
jitter = random.uniform(0, 0.3)
|
|
36
|
-
delay = base_delay * (2 ** (attempt - 1)) + jitter
|
|
37
|
-
logger.warning(
|
|
38
|
-
"retry warn: name=%s attempt=%d/%d delay=%.2fs error=%s",
|
|
39
|
-
name, attempt, retries, delay, str(e),
|
|
40
|
-
exc_info=e
|
|
41
|
-
)
|
|
42
|
-
await asyncio.sleep(delay)
|
|
43
|
-
|
|
44
|
-
# 최종 실패
|
|
45
|
-
if last_err is not None:
|
|
46
|
-
logger.error(
|
|
47
|
-
"FATAL: retry failed: name=%s retries=%s error=%s",
|
|
48
|
-
name, retries, str(last_err), exc_info=last_err
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if fallback is not None:
|
|
52
|
-
try:
|
|
53
|
-
return fallback()
|
|
54
|
-
except Exception as fb_err:
|
|
55
|
-
logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
|
|
56
|
-
return None
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
def _to_jsonable(value: Any) -> Any:
|
|
60
|
-
try:
|
|
61
|
-
if value is None or isinstance(value, (str, int, float, bool)):
|
|
62
|
-
return value
|
|
63
|
-
if isinstance(value, dict):
|
|
64
|
-
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
65
|
-
if isinstance(value, (list, tuple, set)):
|
|
66
|
-
return [_to_jsonable(v) for v in list(value)]
|
|
67
|
-
if hasattr(value, "__dict__"):
|
|
68
|
-
return _to_jsonable(vars(value))
|
|
69
|
-
return repr(value)
|
|
70
|
-
except Exception:
|
|
71
|
-
return repr(value)
|
|
72
|
-
|
|
73
|
-
# ------------------------------ DB Client ------------------------------
|
|
74
|
-
_supabase_client: Optional[Client] = None
|
|
75
|
-
|
|
76
|
-
def initialize_db() -> None:
|
|
77
|
-
global _supabase_client
|
|
78
|
-
if _supabase_client is not None:
|
|
79
|
-
return
|
|
80
|
-
try:
|
|
81
|
-
if os.getenv("ENV") != "production":
|
|
82
|
-
load_dotenv()
|
|
83
|
-
|
|
84
|
-
supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
|
|
85
|
-
supabase_key = os.getenv("SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
|
|
86
|
-
logger.info(
|
|
87
|
-
"[SUPABASE 연결정보]\n URL: %s\n KEY: %s\n",
|
|
88
|
-
supabase_url,
|
|
89
|
-
supabase_key
|
|
90
|
-
)
|
|
91
|
-
if not supabase_url or not supabase_key:
|
|
92
|
-
raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
|
|
93
|
-
_supabase_client = create_client(supabase_url, supabase_key)
|
|
94
|
-
except Exception as e:
|
|
95
|
-
logger.error("initialize_db failed: %s", str(e), exc_info=e)
|
|
96
|
-
raise
|
|
97
|
-
|
|
98
|
-
def get_db_client() -> Client:
|
|
99
|
-
if _supabase_client is None:
|
|
100
|
-
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
101
|
-
return _supabase_client
|
|
102
|
-
|
|
103
|
-
def get_consumer_id() -> str:
|
|
104
|
-
env_consumer = os.getenv("CONSUMER_ID")
|
|
105
|
-
if env_consumer:
|
|
106
|
-
return env_consumer
|
|
107
|
-
host = socket.gethostname()
|
|
108
|
-
pid = os.getpid()
|
|
109
|
-
return f"{host}:{pid}"
|
|
110
|
-
|
|
111
|
-
# ------------------------------ Polling ------------------------------
|
|
112
|
-
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
113
|
-
"""단일 RPC(fetch_pending_task) 호출: p_env 로 dev/prod 분기"""
|
|
114
|
-
if agent_orch is None:
|
|
115
|
-
agent_orch = ""
|
|
116
|
-
if consumer is None:
|
|
117
|
-
consumer = ""
|
|
118
|
-
|
|
119
|
-
def _call():
|
|
120
|
-
client = get_db_client()
|
|
121
|
-
consumer_id = consumer or socket.gethostname()
|
|
122
|
-
|
|
123
|
-
# ENV 값을 dev / (그외=prod) 로만 정규화
|
|
124
|
-
p_env = (os.getenv("ENV") or "").lower()
|
|
125
|
-
if p_env != "dev":
|
|
126
|
-
p_env = "prod"
|
|
127
|
-
|
|
128
|
-
logger.info("\n🔍 [폴링 시작] 작업 대기 중...")
|
|
129
|
-
logger.info("agent_orch=%s, consumer_id=%s, p_env=%s, p_limit=%d", agent_orch, get_consumer_id(), p_env, 1)
|
|
130
|
-
resp = client.rpc(
|
|
131
|
-
"fetch_pending_task",
|
|
132
|
-
{
|
|
133
|
-
"p_agent_orch": agent_orch,
|
|
134
|
-
"p_consumer": consumer_id,
|
|
135
|
-
"p_limit": 1,
|
|
136
|
-
"p_env": p_env,
|
|
137
|
-
},
|
|
138
|
-
).execute()
|
|
139
|
-
|
|
140
|
-
rows = resp.data or []
|
|
141
|
-
if not rows:
|
|
142
|
-
logger.info("\n❌ [폴링 결과 없음] 작업 대기 중...")
|
|
143
|
-
return None
|
|
144
|
-
|
|
145
|
-
row = rows[0]
|
|
146
|
-
# 빈 값들을 NULL로 변환
|
|
147
|
-
if row.get("feedback") in ([], {}):
|
|
148
|
-
row["feedback"] = None
|
|
149
|
-
if row.get("output") in ([], {}):
|
|
150
|
-
row["output"] = None
|
|
151
|
-
if row.get("draft") in ([], {}):
|
|
152
|
-
row["draft"] = None
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
row
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if not
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from supabase import Client, create_client
|
|
9
|
+
import logging
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# ------------------------------ Retry & JSON utils ------------------------------
|
|
16
|
+
async def _async_retry(
|
|
17
|
+
fn: Callable[[], Any],
|
|
18
|
+
*,
|
|
19
|
+
name: str,
|
|
20
|
+
retries: int = 3,
|
|
21
|
+
base_delay: float = 0.8,
|
|
22
|
+
fallback: Optional[Callable[[], Any]] = None,
|
|
23
|
+
) -> Optional[Any]:
|
|
24
|
+
"""
|
|
25
|
+
- 각 시도 실패: warning 로깅(시도/지연/에러 포함)
|
|
26
|
+
- 최종 실패: FATAL 로깅(스택 포함), 예외는 재전파하지 않고 None 반환(기존 정책 유지)
|
|
27
|
+
- fallback 이 있으면 실행(실패 시에도 로깅 후 None)
|
|
28
|
+
"""
|
|
29
|
+
last_err: Optional[Exception] = None
|
|
30
|
+
for attempt in range(1, retries + 1):
|
|
31
|
+
try:
|
|
32
|
+
return await asyncio.to_thread(fn)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
last_err = e
|
|
35
|
+
jitter = random.uniform(0, 0.3)
|
|
36
|
+
delay = base_delay * (2 ** (attempt - 1)) + jitter
|
|
37
|
+
logger.warning(
|
|
38
|
+
"retry warn: name=%s attempt=%d/%d delay=%.2fs error=%s",
|
|
39
|
+
name, attempt, retries, delay, str(e),
|
|
40
|
+
exc_info=e
|
|
41
|
+
)
|
|
42
|
+
await asyncio.sleep(delay)
|
|
43
|
+
|
|
44
|
+
# 최종 실패
|
|
45
|
+
if last_err is not None:
|
|
46
|
+
logger.error(
|
|
47
|
+
"FATAL: retry failed: name=%s retries=%s error=%s",
|
|
48
|
+
name, retries, str(last_err), exc_info=last_err
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if fallback is not None:
|
|
52
|
+
try:
|
|
53
|
+
return fallback()
|
|
54
|
+
except Exception as fb_err:
|
|
55
|
+
logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
|
|
56
|
+
return None
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _to_jsonable(value: Any) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
62
|
+
return value
|
|
63
|
+
if isinstance(value, dict):
|
|
64
|
+
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
65
|
+
if isinstance(value, (list, tuple, set)):
|
|
66
|
+
return [_to_jsonable(v) for v in list(value)]
|
|
67
|
+
if hasattr(value, "__dict__"):
|
|
68
|
+
return _to_jsonable(vars(value))
|
|
69
|
+
return repr(value)
|
|
70
|
+
except Exception:
|
|
71
|
+
return repr(value)
|
|
72
|
+
|
|
73
|
+
# ------------------------------ DB Client ------------------------------
|
|
74
|
+
_supabase_client: Optional[Client] = None
|
|
75
|
+
|
|
76
|
+
def initialize_db() -> None:
|
|
77
|
+
global _supabase_client
|
|
78
|
+
if _supabase_client is not None:
|
|
79
|
+
return
|
|
80
|
+
try:
|
|
81
|
+
if os.getenv("ENV") != "production":
|
|
82
|
+
load_dotenv()
|
|
83
|
+
|
|
84
|
+
supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
|
|
85
|
+
supabase_key = os.getenv("SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
|
|
86
|
+
logger.info(
|
|
87
|
+
"[SUPABASE 연결정보]\n URL: %s\n KEY: %s\n",
|
|
88
|
+
supabase_url,
|
|
89
|
+
supabase_key
|
|
90
|
+
)
|
|
91
|
+
if not supabase_url or not supabase_key:
|
|
92
|
+
raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
|
|
93
|
+
_supabase_client = create_client(supabase_url, supabase_key)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error("initialize_db failed: %s", str(e), exc_info=e)
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def get_db_client() -> Client:
|
|
99
|
+
if _supabase_client is None:
|
|
100
|
+
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
101
|
+
return _supabase_client
|
|
102
|
+
|
|
103
|
+
def get_consumer_id() -> str:
|
|
104
|
+
env_consumer = os.getenv("CONSUMER_ID")
|
|
105
|
+
if env_consumer:
|
|
106
|
+
return env_consumer
|
|
107
|
+
host = socket.gethostname()
|
|
108
|
+
pid = os.getpid()
|
|
109
|
+
return f"{host}:{pid}"
|
|
110
|
+
|
|
111
|
+
# ------------------------------ Polling ------------------------------
|
|
112
|
+
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
113
|
+
"""단일 RPC(fetch_pending_task) 호출: p_env 로 dev/prod 분기"""
|
|
114
|
+
if agent_orch is None:
|
|
115
|
+
agent_orch = ""
|
|
116
|
+
if consumer is None:
|
|
117
|
+
consumer = ""
|
|
118
|
+
|
|
119
|
+
def _call():
|
|
120
|
+
client = get_db_client()
|
|
121
|
+
consumer_id = consumer or socket.gethostname()
|
|
122
|
+
|
|
123
|
+
# ENV 값을 dev / (그외=prod) 로만 정규화
|
|
124
|
+
p_env = (os.getenv("ENV") or "").lower()
|
|
125
|
+
if p_env != "dev":
|
|
126
|
+
p_env = "prod"
|
|
127
|
+
|
|
128
|
+
logger.info("\n🔍 [폴링 시작] 작업 대기 중...")
|
|
129
|
+
logger.info("agent_orch=%s, consumer_id=%s, p_env=%s, p_limit=%d", agent_orch, get_consumer_id(), p_env, 1)
|
|
130
|
+
resp = client.rpc(
|
|
131
|
+
"fetch_pending_task",
|
|
132
|
+
{
|
|
133
|
+
"p_agent_orch": agent_orch,
|
|
134
|
+
"p_consumer": consumer_id,
|
|
135
|
+
"p_limit": 1,
|
|
136
|
+
"p_env": p_env,
|
|
137
|
+
},
|
|
138
|
+
).execute()
|
|
139
|
+
|
|
140
|
+
rows = resp.data or []
|
|
141
|
+
if not rows:
|
|
142
|
+
logger.info("\n❌ [폴링 결과 없음] 작업 대기 중...")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
row = rows[0]
|
|
146
|
+
# 빈 값들을 NULL로 변환
|
|
147
|
+
if row.get("feedback") in ([], {}):
|
|
148
|
+
row["feedback"] = None
|
|
149
|
+
if row.get("output") in ([], {}):
|
|
150
|
+
row["output"] = None
|
|
151
|
+
if row.get("draft") in ([], {}):
|
|
152
|
+
row["draft"] = None
|
|
153
|
+
|
|
154
|
+
if agent_orch == "browser-automation-agent":
|
|
155
|
+
resp = (
|
|
156
|
+
client.table("env").select("value").eq("key", "browser_use").eq("tenant_id", row["tenant_id"] or "").execute()
|
|
157
|
+
)
|
|
158
|
+
data = (resp.data or [])
|
|
159
|
+
logger.info("browser_use_sensitive_data: %s", data)
|
|
160
|
+
if not data:
|
|
161
|
+
row["sensitive_data"] = "{}"
|
|
162
|
+
else:
|
|
163
|
+
row["sensitive_data"] = data[0].get("value") if data else None
|
|
164
|
+
|
|
165
|
+
logger.info("browser_use_sensitive_data: %s", row["sensitive_data"])
|
|
166
|
+
|
|
167
|
+
return row
|
|
168
|
+
|
|
169
|
+
return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ------------------------------ Fetch Single Todo ------------------------------
|
|
173
|
+
async def fetch_todo_by_id(todo_id: str) -> Optional[Dict[str, Any]]:
|
|
174
|
+
"""todo_id로 단건 조회 후 컨텍스트 준비에 필요한 형태로 정규화합니다.
|
|
175
|
+
|
|
176
|
+
- 어떤 필드도 업데이트하지 않고, 조건 없이 id로만 조회합니다.
|
|
177
|
+
"""
|
|
178
|
+
if not todo_id:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _call():
|
|
182
|
+
client = get_db_client()
|
|
183
|
+
resp = (
|
|
184
|
+
client.table("todolist")
|
|
185
|
+
.select("*")
|
|
186
|
+
.eq("id", todo_id)
|
|
187
|
+
.single()
|
|
188
|
+
.execute()
|
|
189
|
+
)
|
|
190
|
+
row = getattr(resp, "data", None)
|
|
191
|
+
if not row:
|
|
192
|
+
return None
|
|
193
|
+
return row
|
|
194
|
+
|
|
195
|
+
row = await _async_retry(_call, name="fetch_todo_by_id", fallback=lambda: None)
|
|
196
|
+
if not row:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# 빈 컨테이너 정규화
|
|
200
|
+
if row.get("feedback") in ([], {}):
|
|
201
|
+
row["feedback"] = None
|
|
202
|
+
if row.get("output") in ([], {}):
|
|
203
|
+
row["output"] = None
|
|
204
|
+
if row.get("draft") in ([], {}):
|
|
205
|
+
row["draft"] = None
|
|
206
|
+
|
|
207
|
+
return row
|
|
208
|
+
|
|
209
|
+
# ------------------------------ Events & Results ------------------------------
|
|
210
|
+
async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
|
|
211
|
+
"""이벤트 다건 저장 함수"""
|
|
212
|
+
|
|
213
|
+
if not payloads:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
safe_list: List[Dict[str, Any]] = []
|
|
217
|
+
for p in payloads:
|
|
218
|
+
sp = _to_jsonable(p)
|
|
219
|
+
if isinstance(sp, dict) and sp.get("status", "") == "":
|
|
220
|
+
sp["status"] = None
|
|
221
|
+
safe_list.append(sp)
|
|
222
|
+
|
|
223
|
+
def _call():
|
|
224
|
+
client = get_db_client()
|
|
225
|
+
return client.rpc("record_events_bulk", {"p_events": safe_list}).execute()
|
|
226
|
+
|
|
227
|
+
res = await _async_retry(_call, name="record_events_bulk", fallback=lambda: None)
|
|
228
|
+
if res is None:
|
|
229
|
+
logger.error("❌ record_events_bulk failed: events not persisted count=%d", len(safe_list))
|
|
230
|
+
else:
|
|
231
|
+
logger.info("record_events_bulk ok: count=%d", len(safe_list))
|
|
232
|
+
|
|
233
|
+
async def record_event(payload: Dict[str, Any]) -> None:
|
|
234
|
+
"""단건 이벤트 저장 함수"""
|
|
235
|
+
|
|
236
|
+
if not payload:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
def _call():
|
|
240
|
+
client = get_db_client()
|
|
241
|
+
safe_payload = _to_jsonable(payload)
|
|
242
|
+
if isinstance(safe_payload, dict) and safe_payload.get("status", "") == "":
|
|
243
|
+
safe_payload["status"] = None
|
|
244
|
+
return client.table("events").insert(safe_payload).execute()
|
|
245
|
+
|
|
246
|
+
res = await _async_retry(_call, name="record_event", fallback=lambda: None)
|
|
247
|
+
if res is None:
|
|
248
|
+
logger.error("❌ record_event failed =%s", payload.get("event_type"))
|
|
249
|
+
else:
|
|
250
|
+
logger.info("record_event ok: event_type=%s", payload.get("event_type"))
|
|
251
|
+
|
|
252
|
+
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
253
|
+
"""결과 저장 함수"""
|
|
254
|
+
|
|
255
|
+
if not todo_id:
|
|
256
|
+
logger.error("save_task_result invalid todo_id: %s", str(todo_id))
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
def _safe(val: Any) -> Any:
|
|
260
|
+
try:
|
|
261
|
+
return _to_jsonable(val)
|
|
262
|
+
except Exception:
|
|
263
|
+
try:
|
|
264
|
+
return {"repr": repr(val)}
|
|
265
|
+
except Exception:
|
|
266
|
+
return {"error": "unserializable payload"}
|
|
267
|
+
|
|
268
|
+
def _call():
|
|
269
|
+
client = get_db_client()
|
|
270
|
+
payload = _safe(result)
|
|
271
|
+
return client.rpc("save_task_result", {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)}).execute()
|
|
272
|
+
|
|
273
|
+
res = await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
274
|
+
if res is None:
|
|
275
|
+
logger.error("❌ save_task_result failed todo_id=%s", todo_id)
|
|
276
|
+
else:
|
|
277
|
+
logger.info("save_task_result ok todo_id=%s", todo_id)
|
|
278
|
+
|
|
279
|
+
# ------------------------------ Failure Status ------------------------------
|
|
280
|
+
async def update_task_error(todo_id: str) -> None:
|
|
281
|
+
"""작업 실패 상태 업데이트 함수"""
|
|
282
|
+
|
|
283
|
+
if not todo_id:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
def _call():
|
|
287
|
+
client = get_db_client()
|
|
288
|
+
return client.table("todolist").update({"draft_status": "FAILED", "consumer": None}).eq("id", todo_id).execute()
|
|
289
|
+
|
|
290
|
+
res = await _async_retry(_call, name="update_task_error", fallback=lambda: None)
|
|
291
|
+
if res is None:
|
|
292
|
+
logger.error("❌ update_task_error failed todo_id=%s", todo_id)
|
|
293
|
+
else:
|
|
294
|
+
logger.info("update_task_error ok todo_id=%s", todo_id)
|
|
295
|
+
|
|
296
|
+
# ============================== Prepare Context ==============================
|
|
297
|
+
|
|
298
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
299
|
+
|
|
300
|
+
async def fetch_form_def(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
|
|
301
|
+
"""폼 정의 조회 함수"""
|
|
302
|
+
form_id = (tool_val or "").replace("formHandler:", "", 1)
|
|
303
|
+
|
|
304
|
+
def _call():
|
|
305
|
+
client = get_db_client()
|
|
306
|
+
resp = (
|
|
307
|
+
client.table("form_def")
|
|
308
|
+
.select("fields_json, html")
|
|
309
|
+
.eq("id", form_id)
|
|
310
|
+
.eq("tenant_id", tenant_id or "")
|
|
311
|
+
.execute()
|
|
312
|
+
)
|
|
313
|
+
data = (resp.data or [])
|
|
314
|
+
if not data:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
row = data[0]
|
|
318
|
+
return {
|
|
319
|
+
"fields": row.get("fields_json"),
|
|
320
|
+
"html": row.get("html"),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
res = await _async_retry(_call, name="fetch_form_def")
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error("fetch_form_def fatal: %s", str(e), exc_info=e)
|
|
327
|
+
res = None
|
|
328
|
+
|
|
329
|
+
if not res or not res.get("fields"):
|
|
330
|
+
# 기본(자유형식) 폼
|
|
331
|
+
return (
|
|
332
|
+
form_id or "freeform",
|
|
333
|
+
[{"key": "freeform", "type": "textarea", "text": "자유형식 입력", "placeholder": "원하는 내용을 자유롭게 입력해주세요."}],
|
|
334
|
+
None,
|
|
335
|
+
)
|
|
336
|
+
return (form_id or "freeform", res["fields"], res.get("html"))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def fetch_users_grouped(user_ids: List[str]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
340
|
+
"""해당 todo에서 사용자 목록과 에이전트 목록 조회하는는 함수"""
|
|
341
|
+
ids = [u for u in (user_ids or []) if u]
|
|
342
|
+
if not ids:
|
|
343
|
+
return ([], [])
|
|
344
|
+
|
|
345
|
+
def _call():
|
|
346
|
+
client = get_db_client()
|
|
347
|
+
resp = (
|
|
348
|
+
client.table("users")
|
|
349
|
+
.select("*")
|
|
350
|
+
.in_("id", ids)
|
|
351
|
+
.execute()
|
|
352
|
+
)
|
|
353
|
+
rows = resp.data or []
|
|
354
|
+
return rows
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
rows = await _async_retry(_call, name="fetch_users_grouped", fallback=lambda: [])
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error("fetch_users_grouped fatal: %s", str(e), exc_info=e)
|
|
360
|
+
rows = []
|
|
361
|
+
|
|
362
|
+
agents, users = [], []
|
|
363
|
+
for r in rows:
|
|
364
|
+
if r.get("is_agent") is True:
|
|
365
|
+
agents.append(r)
|
|
366
|
+
else:
|
|
367
|
+
users.append(r)
|
|
368
|
+
return (agents, users)
|
|
369
|
+
|
|
370
|
+
async def fetch_email_users_by_proc_inst_id(proc_inst_id: str) -> str:
|
|
371
|
+
"""proc_inst_id로 이메일 수집(사람만): todolist → users(in) 한 번에"""
|
|
372
|
+
if not proc_inst_id:
|
|
373
|
+
return ""
|
|
374
|
+
|
|
375
|
+
def _call():
|
|
376
|
+
client = get_db_client()
|
|
377
|
+
# 3-1) 해당 인스턴스의 user_id 수집(중복 제거)
|
|
378
|
+
tl = (
|
|
379
|
+
client.table("todolist")
|
|
380
|
+
.select("user_id")
|
|
381
|
+
.eq("proc_inst_id", proc_inst_id)
|
|
382
|
+
.execute()
|
|
383
|
+
)
|
|
384
|
+
ids_set = set()
|
|
385
|
+
for row in (tl.data or []):
|
|
386
|
+
uid_csv = (row.get("user_id") or "").strip()
|
|
387
|
+
if not uid_csv:
|
|
388
|
+
continue
|
|
389
|
+
# user_id는 문자열 CSV라고 전제
|
|
390
|
+
for uid in uid_csv.split(","):
|
|
391
|
+
u = uid.strip()
|
|
392
|
+
if u:
|
|
393
|
+
ids_set.add(u)
|
|
394
|
+
if not ids_set:
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
# 3-2) 한 번의 IN 조회로 사람만 이메일 추출
|
|
398
|
+
ur = (
|
|
399
|
+
client.table("users")
|
|
400
|
+
.select("id, email, is_agent")
|
|
401
|
+
.in_("id", list(ids_set))
|
|
402
|
+
.eq("is_agent", False)
|
|
403
|
+
.execute()
|
|
404
|
+
)
|
|
405
|
+
emails = []
|
|
406
|
+
for u in (ur.data or []):
|
|
407
|
+
email = (u.get("email") or "").strip()
|
|
408
|
+
if email:
|
|
409
|
+
emails.append(email)
|
|
410
|
+
# 중복 제거 및 정렬(보기 좋게)
|
|
411
|
+
return sorted(set(emails))
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
emails = await _async_retry(_call, name="fetch_email_users_by_proc_inst_id", fallback=lambda: [])
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error("fetch_email_users_by_proc_inst_id fatal: %s", str(e), exc_info=e)
|
|
417
|
+
emails = []
|
|
418
|
+
|
|
419
|
+
return ",".join(emails) if emails else ""
|
|
420
|
+
|
|
421
|
+
async def fetch_tenant_mcp(tenant_id: str) -> Optional[Dict[str, Any]]:
|
|
422
|
+
"""mcp 설정 조회 함수"""
|
|
423
|
+
if not tenant_id:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
def _call():
|
|
427
|
+
client = get_db_client()
|
|
428
|
+
return (
|
|
429
|
+
client.table("tenants")
|
|
430
|
+
.select("mcp")
|
|
431
|
+
.eq("id", tenant_id)
|
|
432
|
+
.single()
|
|
433
|
+
.execute()
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
resp = await _async_retry(_call, name="fetch_tenant_mcp", fallback=lambda: None)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error("fetch_tenant_mcp fatal: %s", str(e), exc_info=e)
|
|
440
|
+
return None
|
|
441
|
+
|
|
429
442
|
return resp.data.get("mcp") if resp and getattr(resp, "data", None) else None
|