process-gpt-agent-sdk 0.2.10__py3-none-any.whl → 0.3.10__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.10.dist-info/METADATA +336 -0
- process_gpt_agent_sdk-0.3.10.dist-info/RECORD +5 -0
- processgpt_agent_sdk/processgpt_agent_framework.py +402 -0
- process_gpt_agent_sdk-0.2.10.dist-info/METADATA +0 -1026
- process_gpt_agent_sdk-0.2.10.dist-info/RECORD +0 -19
- processgpt_agent_sdk/__init__.py +0 -11
- processgpt_agent_sdk/core/__init__.py +0 -0
- processgpt_agent_sdk/core/database.py +0 -464
- processgpt_agent_sdk/server.py +0 -313
- processgpt_agent_sdk/simulator.py +0 -231
- processgpt_agent_sdk/tools/__init__.py +0 -0
- processgpt_agent_sdk/tools/human_query_tool.py +0 -211
- processgpt_agent_sdk/tools/knowledge_tools.py +0 -206
- processgpt_agent_sdk/tools/safe_tool_loader.py +0 -209
- processgpt_agent_sdk/utils/__init__.py +0 -0
- processgpt_agent_sdk/utils/context_manager.py +0 -45
- processgpt_agent_sdk/utils/crewai_event_listener.py +0 -205
- processgpt_agent_sdk/utils/event_handler.py +0 -72
- processgpt_agent_sdk/utils/logger.py +0 -97
- processgpt_agent_sdk/utils/summarizer.py +0 -146
- {process_gpt_agent_sdk-0.2.10.dist-info → process_gpt_agent_sdk-0.3.10.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.2.10.dist-info → process_gpt_agent_sdk-0.3.10.dist-info}/top_level.txt +0 -0
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
processgpt_agent_sdk/__init__.py,sha256=zQHVsiKXSn5cZ1OqhptxbOfoNLaq_VkAIECjRLnmW60,371
|
|
2
|
-
processgpt_agent_sdk/server.py,sha256=RXehJ8RS1ctbyK03MmZp6nWd5HTGBmI6bMITcVxiVug,15268
|
|
3
|
-
processgpt_agent_sdk/simulator.py,sha256=CxWjBH0BGmmAvBvzYveoSToB7LeJqIC6phT7o81WQB8,10194
|
|
4
|
-
processgpt_agent_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
processgpt_agent_sdk/core/database.py,sha256=HLf7joKK6kl9wrpFqVqYk6busmZPXeFTi2cstkb8ffY,16834
|
|
6
|
-
processgpt_agent_sdk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
processgpt_agent_sdk/tools/human_query_tool.py,sha256=jI5lJJ-JoW-QRYY8GPkrMBPIFgN_pb4NtS4LQyC9ulk,11082
|
|
8
|
-
processgpt_agent_sdk/tools/knowledge_tools.py,sha256=xa0zncJPjmbd0bo5fjMUF6xY45SYlc0a1PSVx1aTtVs,9143
|
|
9
|
-
processgpt_agent_sdk/tools/safe_tool_loader.py,sha256=3PN6bwcTiyzjh_NqOVmn3I4P4VUm6UsZZaIGgGv1Qsc,7872
|
|
10
|
-
processgpt_agent_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
processgpt_agent_sdk/utils/context_manager.py,sha256=1_WYpBg7jVlQKn_6_7Ep7mAqCeXeLcxoykdyVCD5fp0,1849
|
|
12
|
-
processgpt_agent_sdk/utils/crewai_event_listener.py,sha256=PutCKn3L6Au9nRbUDBV2tv3DZXLz24T4HQWJaA0_Dxk,9360
|
|
13
|
-
processgpt_agent_sdk/utils/event_handler.py,sha256=_S_xUMGEp9GUMIkWpJiW7j5lywRAwJzn9CF82cmtKbQ,2951
|
|
14
|
-
processgpt_agent_sdk/utils/logger.py,sha256=jW4z0Wn88vPfXkOVg5l8VA6O4544OhnqrU17M2zr_Ss,3390
|
|
15
|
-
processgpt_agent_sdk/utils/summarizer.py,sha256=f3AnbeMkTG-KffFrM9aiBxl0a01VMi8M1hYlS7rfcNo,5889
|
|
16
|
-
process_gpt_agent_sdk-0.2.10.dist-info/METADATA,sha256=TYBHS7__pbob8y_RWDpihL0QUHtB3xBnF2HZdBynh70,34136
|
|
17
|
-
process_gpt_agent_sdk-0.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
-
process_gpt_agent_sdk-0.2.10.dist-info/top_level.txt,sha256=Xe6zrj3_3Vv7d0pl5RRtenVUckwOVBVLQn2P03j5REo,21
|
|
19
|
-
process_gpt_agent_sdk-0.2.10.dist-info/RECORD,,
|
processgpt_agent_sdk/__init__.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from .server import ProcessGPTAgentServer, ProcessGPTRequestContext, ProcessGPTEventQueue
|
|
2
|
-
from .simulator import ProcessGPTAgentSimulator, SimulatorRequestContext, SimulatorEventQueue
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"ProcessGPTAgentServer",
|
|
6
|
-
"ProcessGPTRequestContext",
|
|
7
|
-
"ProcessGPTEventQueue",
|
|
8
|
-
"ProcessGPTAgentSimulator",
|
|
9
|
-
"SimulatorRequestContext",
|
|
10
|
-
"SimulatorEventQueue",
|
|
11
|
-
]
|
|
File without changes
|
|
@@ -1,464 +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, Callable, TypeVar
|
|
7
|
-
|
|
8
|
-
from dotenv import load_dotenv
|
|
9
|
-
from supabase import Client, create_client
|
|
10
|
-
import logging
|
|
11
|
-
import random
|
|
12
|
-
|
|
13
|
-
T = TypeVar("T")
|
|
14
|
-
|
|
15
|
-
from ..utils.logger import handle_application_error, write_log_message
|
|
16
|
-
|
|
17
|
-
# ============================================================================
|
|
18
|
-
# Utility: 재시도 헬퍼 및 유틸
|
|
19
|
-
# 설명: 동기 DB 호출을 안전하게 재시도 (지수 백오프 + 지터) 및 유틸
|
|
20
|
-
# ============================================================================
|
|
21
|
-
|
|
22
|
-
async def _async_retry(
|
|
23
|
-
fn: Callable[[], T],
|
|
24
|
-
*,
|
|
25
|
-
name: str,
|
|
26
|
-
retries: int = 3,
|
|
27
|
-
base_delay: float = 0.8,
|
|
28
|
-
fallback: Optional[Callable[[], T]] = None,
|
|
29
|
-
) -> Optional[T]:
|
|
30
|
-
"""지수 백오프+jitter로 재시도하고 실패 시 fallback/None 반환."""
|
|
31
|
-
last_err: Optional[Exception] = None
|
|
32
|
-
for attempt in range(1, retries + 1):
|
|
33
|
-
try:
|
|
34
|
-
return await asyncio.to_thread(fn)
|
|
35
|
-
except Exception as e:
|
|
36
|
-
last_err = e
|
|
37
|
-
jitter = random.uniform(0, 0.3)
|
|
38
|
-
delay = base_delay * (2 ** (attempt - 1)) + jitter
|
|
39
|
-
write_log_message(f"{name} 재시도 {attempt}/{retries} (delay={delay:.2f}s): {e}", level=logging.WARNING)
|
|
40
|
-
await asyncio.sleep(delay)
|
|
41
|
-
write_log_message(f"{name} 최종 실패: {last_err}", level=logging.ERROR)
|
|
42
|
-
if fallback is not None:
|
|
43
|
-
try:
|
|
44
|
-
fb_val = fallback()
|
|
45
|
-
write_log_message(f"{name} 폴백 사용", level=logging.WARNING)
|
|
46
|
-
return fb_val
|
|
47
|
-
except Exception as e:
|
|
48
|
-
write_log_message(f"{name} 폴백 실패: {e}", level=logging.ERROR)
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _is_valid_uuid(value: str) -> bool:
|
|
53
|
-
"""UUID 문자열 형식 검증 (v1~v8 포함)"""
|
|
54
|
-
try:
|
|
55
|
-
uuid.UUID(value)
|
|
56
|
-
return True
|
|
57
|
-
except Exception:
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# ============================================================================
|
|
62
|
-
# DB 연결/클라이언트
|
|
63
|
-
# 설명: 환경 변수 로드, Supabase 클라이언트 초기화/반환, 컨슈머 식별자
|
|
64
|
-
# ============================================================================
|
|
65
|
-
_supabase_client: Optional[Client] = None
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def initialize_db() -> None:
|
|
69
|
-
"""환경변수 로드 및 Supabase 클라이언트 초기화"""
|
|
70
|
-
global _supabase_client
|
|
71
|
-
if _supabase_client is not None:
|
|
72
|
-
return
|
|
73
|
-
if os.getenv("ENV") != "production":
|
|
74
|
-
load_dotenv()
|
|
75
|
-
supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
|
|
76
|
-
supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
|
|
77
|
-
if not supabase_url or not supabase_key:
|
|
78
|
-
raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
|
|
79
|
-
_supabase_client = create_client(supabase_url, supabase_key)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def get_db_client() -> Client:
|
|
83
|
-
"""초기화된 Supabase 클라이언트 반환."""
|
|
84
|
-
if _supabase_client is None:
|
|
85
|
-
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
86
|
-
return _supabase_client
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_consumer_id() -> str:
|
|
90
|
-
"""파드/프로세스 식별자 생성(CONSUMER_ID>HOST:PID)."""
|
|
91
|
-
env_consumer = os.getenv("CONSUMER_ID")
|
|
92
|
-
if env_consumer:
|
|
93
|
-
return env_consumer
|
|
94
|
-
host = socket.gethostname()
|
|
95
|
-
pid = os.getpid()
|
|
96
|
-
return f"{host}:{pid}"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ============================================================================
|
|
100
|
-
# 데이터 조회
|
|
101
|
-
# 설명: TODOLIST 테이블 조회, 완료 output 목록 조회, 이벤트 조회, 폼 조회, 테넌트 MCP 설정 조회, 사용자 및 에이전트 조회
|
|
102
|
-
# ============================================================================
|
|
103
|
-
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
104
|
-
"""TODOLIST 테이블에서 대기중인 워크아이템을 조회"""
|
|
105
|
-
def _call():
|
|
106
|
-
supabase = get_db_client()
|
|
107
|
-
consumer_id = socket.gethostname()
|
|
108
|
-
env = (os.getenv("ENV") or "").lower()
|
|
109
|
-
|
|
110
|
-
if env == "dev":
|
|
111
|
-
# 개발 환경: 특정 테넌트(uengine)만 폴링
|
|
112
|
-
resp = supabase.rpc(
|
|
113
|
-
"fetch_pending_task_dev",
|
|
114
|
-
{"p_limit": 1, "p_consumer": consumer_id, "p_tenant_id": "uengine"},
|
|
115
|
-
).execute()
|
|
116
|
-
else:
|
|
117
|
-
# 운영/기타 환경: 기존 로직 유지
|
|
118
|
-
resp = supabase.rpc(
|
|
119
|
-
"fetch_pending_task",
|
|
120
|
-
{"p_limit": 1, "p_consumer": consumer_id},
|
|
121
|
-
).execute()
|
|
122
|
-
|
|
123
|
-
rows = resp.data or []
|
|
124
|
-
return rows[0] if rows else None
|
|
125
|
-
|
|
126
|
-
resp = await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
|
|
127
|
-
if not resp:
|
|
128
|
-
return None
|
|
129
|
-
return resp
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async def fetch_todo_by_id(todo_id: str) -> Optional[Dict[str, Any]]:
|
|
133
|
-
"""특정 todo id로 todolist의 단건을 조회"""
|
|
134
|
-
if not todo_id:
|
|
135
|
-
return None
|
|
136
|
-
def _call():
|
|
137
|
-
client = get_db_client()
|
|
138
|
-
return (
|
|
139
|
-
client.table("todolist").select("*").eq("id", todo_id).single().execute()
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
resp = await _async_retry(_call, name="fetch_todo_by_id")
|
|
143
|
-
if not resp or not resp.data:
|
|
144
|
-
return None
|
|
145
|
-
return resp.data
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
async def fetch_done_data(proc_inst_id: Optional[str]) -> List[Any]:
|
|
149
|
-
"""proc_inst_id로 완료된 워크아이템의 output 목록을 조회"""
|
|
150
|
-
if not proc_inst_id:
|
|
151
|
-
return []
|
|
152
|
-
def _call():
|
|
153
|
-
client = get_db_client()
|
|
154
|
-
return client.rpc("fetch_done_data", {"p_proc_inst_id": proc_inst_id}).execute()
|
|
155
|
-
|
|
156
|
-
resp = await _async_retry(_call, name="fetch_done_data", fallback=lambda: None)
|
|
157
|
-
if not resp:
|
|
158
|
-
return []
|
|
159
|
-
return [row.get("output") for row in (resp.data or []) if row.get("output")]
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def fetch_human_response_sync(job_id: str) -> Optional[Dict[str, Any]]:
|
|
163
|
-
"""events에서 특정 job_id의 human_response 조회"""
|
|
164
|
-
if not job_id:
|
|
165
|
-
return None
|
|
166
|
-
try:
|
|
167
|
-
client = get_db_client()
|
|
168
|
-
resp = (
|
|
169
|
-
client
|
|
170
|
-
.table("events")
|
|
171
|
-
.select("*")
|
|
172
|
-
.eq("job_id", job_id)
|
|
173
|
-
.eq("event_type", "human_response")
|
|
174
|
-
.execute()
|
|
175
|
-
)
|
|
176
|
-
rows = resp.data or []
|
|
177
|
-
return rows[0] if rows else None
|
|
178
|
-
except Exception as e:
|
|
179
|
-
handle_application_error("fetch_human_response_sync 실패", e, raise_error=False)
|
|
180
|
-
return None
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
async def fetch_task_status(todo_id: str) -> Optional[str]:
|
|
184
|
-
"""todo의 draft_status를 조회한다."""
|
|
185
|
-
def _call():
|
|
186
|
-
client = get_db_client()
|
|
187
|
-
return (
|
|
188
|
-
client.table("todolist").select("draft_status").eq("id", todo_id).single().execute()
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
resp = await _async_retry(_call, name="fetch_task_status")
|
|
192
|
-
if not resp or not resp.data:
|
|
193
|
-
return None
|
|
194
|
-
return resp.data.get("draft_status")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
async def fetch_all_agents() -> List[Dict[str, Any]]:
|
|
199
|
-
"""모든 에이전트 목록을 정규화하여 반환한다."""
|
|
200
|
-
def _call():
|
|
201
|
-
client = get_db_client()
|
|
202
|
-
return (
|
|
203
|
-
client.table("users")
|
|
204
|
-
.select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
|
|
205
|
-
.eq("is_agent", True)
|
|
206
|
-
.execute()
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
resp = await _async_retry(_call, name="fetch_all_agents")
|
|
210
|
-
rows = resp.data or [] if resp else []
|
|
211
|
-
normalized: List[Dict[str, Any]] = []
|
|
212
|
-
for row in rows:
|
|
213
|
-
normalized.append(
|
|
214
|
-
{
|
|
215
|
-
"id": row.get("id"),
|
|
216
|
-
"name": row.get("username"),
|
|
217
|
-
"role": row.get("role"),
|
|
218
|
-
"goal": row.get("goal"),
|
|
219
|
-
"persona": row.get("persona"),
|
|
220
|
-
"tools": row.get("tools") or "mem0",
|
|
221
|
-
"profile": row.get("profile"),
|
|
222
|
-
"model": row.get("model"),
|
|
223
|
-
"tenant_id": row.get("tenant_id"),
|
|
224
|
-
}
|
|
225
|
-
)
|
|
226
|
-
return normalized
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
async def fetch_agent_data(user_ids: str) -> List[Dict[str, Any]]:
|
|
230
|
-
"""TODOLIST의 user_id 값으로, 역할로 지정된 에이전트를 조회하고 정규화해 반환한다."""
|
|
231
|
-
|
|
232
|
-
raw_ids = [x.strip() for x in (user_ids or "").split(",") if x.strip()]
|
|
233
|
-
valid_ids = [x for x in raw_ids if _is_valid_uuid(x)]
|
|
234
|
-
|
|
235
|
-
if not valid_ids:
|
|
236
|
-
return await fetch_all_agents()
|
|
237
|
-
|
|
238
|
-
def _call():
|
|
239
|
-
client = get_db_client()
|
|
240
|
-
resp = (
|
|
241
|
-
client
|
|
242
|
-
.table("users")
|
|
243
|
-
.select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
|
|
244
|
-
.in_("id", valid_ids)
|
|
245
|
-
.eq("is_agent", True)
|
|
246
|
-
.execute()
|
|
247
|
-
)
|
|
248
|
-
rows = resp.data or []
|
|
249
|
-
normalized: List[Dict[str, Any]] = []
|
|
250
|
-
for row in rows:
|
|
251
|
-
normalized.append(
|
|
252
|
-
{
|
|
253
|
-
"id": row.get("id"),
|
|
254
|
-
"name": row.get("username"),
|
|
255
|
-
"role": row.get("role"),
|
|
256
|
-
"goal": row.get("goal"),
|
|
257
|
-
"persona": row.get("persona"),
|
|
258
|
-
"tools": row.get("tools") or "mem0",
|
|
259
|
-
"profile": row.get("profile"),
|
|
260
|
-
"model": row.get("model"),
|
|
261
|
-
"tenant_id": row.get("tenant_id"),
|
|
262
|
-
}
|
|
263
|
-
)
|
|
264
|
-
return normalized
|
|
265
|
-
|
|
266
|
-
result = await _async_retry(_call, name="fetch_agent_data", fallback=lambda: [])
|
|
267
|
-
|
|
268
|
-
if not result:
|
|
269
|
-
return await fetch_all_agents()
|
|
270
|
-
|
|
271
|
-
return result
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
async def fetch_form_types(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
|
|
275
|
-
"""폼 타입 정의를 조회해 (form_id, fields, html)로 반환한다."""
|
|
276
|
-
form_id = tool_val[12:] if tool_val.startswith("formHandler:") else tool_val
|
|
277
|
-
|
|
278
|
-
def _call():
|
|
279
|
-
client = get_db_client()
|
|
280
|
-
resp = (
|
|
281
|
-
client
|
|
282
|
-
.table("form_def")
|
|
283
|
-
.select("fields_json, html")
|
|
284
|
-
.eq("id", form_id)
|
|
285
|
-
.eq("tenant_id", tenant_id)
|
|
286
|
-
.execute()
|
|
287
|
-
)
|
|
288
|
-
fields_json = resp.data[0].get("fields_json") if resp.data else None
|
|
289
|
-
form_html = resp.data[0].get("html") if resp.data else None
|
|
290
|
-
if not fields_json:
|
|
291
|
-
return form_id, [{"key": form_id, "type": "default", "text": ""}], form_html
|
|
292
|
-
return form_id, fields_json, form_html
|
|
293
|
-
|
|
294
|
-
resp = await _async_retry(
|
|
295
|
-
_call,
|
|
296
|
-
name="fetch_form_types",
|
|
297
|
-
fallback=lambda: (form_id, [{"key": form_id, "type": "default", "text": ""}], None),
|
|
298
|
-
)
|
|
299
|
-
return resp if resp else (form_id, [{"key": form_id, "type": "default", "text": ""}], None)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
async def fetch_tenant_mcp_config(tenant_id: str) -> Optional[Dict[str, Any]]:
|
|
303
|
-
"""테넌트 MCP 설정을 조회해 반환한다."""
|
|
304
|
-
def _call():
|
|
305
|
-
client = get_db_client()
|
|
306
|
-
return client.table("tenants").select("mcp").eq("id", tenant_id).single().execute()
|
|
307
|
-
|
|
308
|
-
resp = await _async_retry(_call, name="fetch_tenant_mcp_config", fallback=lambda: None)
|
|
309
|
-
return resp.data.get("mcp") if resp and resp.data else None
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
async def fetch_human_users_by_proc_inst_id(proc_inst_id: str) -> str:
|
|
313
|
-
"""proc_inst_id로 현재 프로세스의 모든 사용자 이메일 목록을 쉼표로 반환한다."""
|
|
314
|
-
if not proc_inst_id:
|
|
315
|
-
return ""
|
|
316
|
-
|
|
317
|
-
def _sync():
|
|
318
|
-
try:
|
|
319
|
-
supabase = get_db_client()
|
|
320
|
-
|
|
321
|
-
resp = (
|
|
322
|
-
supabase
|
|
323
|
-
.table('todolist')
|
|
324
|
-
.select('user_id')
|
|
325
|
-
.eq('proc_inst_id', proc_inst_id)
|
|
326
|
-
.execute()
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
if not resp.data:
|
|
330
|
-
return ""
|
|
331
|
-
|
|
332
|
-
all_user_ids = set()
|
|
333
|
-
for row in resp.data:
|
|
334
|
-
user_id = row.get('user_id', '')
|
|
335
|
-
if user_id:
|
|
336
|
-
ids = [id.strip() for id in user_id.split(',') if id.strip()]
|
|
337
|
-
all_user_ids.update(ids)
|
|
338
|
-
|
|
339
|
-
if not all_user_ids:
|
|
340
|
-
return ""
|
|
341
|
-
|
|
342
|
-
human_user_emails = []
|
|
343
|
-
for user_id in all_user_ids:
|
|
344
|
-
if not _is_valid_uuid(user_id):
|
|
345
|
-
continue
|
|
346
|
-
|
|
347
|
-
user_resp = (
|
|
348
|
-
supabase
|
|
349
|
-
.table('users')
|
|
350
|
-
.select('id, email, is_agent')
|
|
351
|
-
.eq('id', user_id)
|
|
352
|
-
.execute()
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
if user_resp.data:
|
|
356
|
-
user = user_resp.data[0]
|
|
357
|
-
is_agent = user.get('is_agent')
|
|
358
|
-
if not is_agent:
|
|
359
|
-
email = (user.get('email') or '').strip()
|
|
360
|
-
if email:
|
|
361
|
-
human_user_emails.append(email)
|
|
362
|
-
|
|
363
|
-
return ','.join(human_user_emails)
|
|
364
|
-
|
|
365
|
-
except Exception as e:
|
|
366
|
-
handle_application_error("사용자조회오류", e, raise_error=False)
|
|
367
|
-
return ""
|
|
368
|
-
|
|
369
|
-
return await asyncio.to_thread(_sync)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# ============================================================================
|
|
373
|
-
# 데이터 저장
|
|
374
|
-
# 설명: 이벤트/알림/작업 결과 저장
|
|
375
|
-
# ============================================================================
|
|
376
|
-
async def record_event(payload: Dict[str, Any]) -> None:
|
|
377
|
-
"""UI용 events 테이블에 이벤트 기록 (전달된 payload 그대로 저장)"""
|
|
378
|
-
def _call():
|
|
379
|
-
client = get_db_client()
|
|
380
|
-
return client.table("events").insert(payload).execute()
|
|
381
|
-
|
|
382
|
-
resp = await _async_retry(_call, name="record_event", fallback=lambda: None)
|
|
383
|
-
if resp is None:
|
|
384
|
-
write_log_message("record_event 최종 실패(무시)", level=logging.WARNING)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
389
|
-
"""작업 결과를 저장한다(중간/최종)."""
|
|
390
|
-
def _call():
|
|
391
|
-
client = get_db_client()
|
|
392
|
-
payload = result if isinstance(result, (dict, list)) else json.loads(json.dumps(result))
|
|
393
|
-
return client.rpc(
|
|
394
|
-
"save_task_result",
|
|
395
|
-
{"p_todo_id": todo_id, "p_payload": payload, "p_final": final},
|
|
396
|
-
).execute()
|
|
397
|
-
|
|
398
|
-
await _async_retry(_call, name="save_task_result", fallback=lambda: None)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def save_notification(
|
|
402
|
-
*,
|
|
403
|
-
title: str,
|
|
404
|
-
notif_type: str,
|
|
405
|
-
description: Optional[str] = None,
|
|
406
|
-
user_ids_csv: Optional[str] = None,
|
|
407
|
-
tenant_id: Optional[str] = None,
|
|
408
|
-
url: Optional[str] = None,
|
|
409
|
-
from_user_id: Optional[str] = None,
|
|
410
|
-
) -> None:
|
|
411
|
-
"""notifications 테이블에 알림 저장"""
|
|
412
|
-
try:
|
|
413
|
-
# 대상 사용자가 없으면 작업 생략
|
|
414
|
-
if not user_ids_csv:
|
|
415
|
-
write_log_message(f"알림 저장 생략: 대상 사용자 없음 (user_ids_csv={user_ids_csv})")
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
supabase = get_db_client()
|
|
419
|
-
|
|
420
|
-
user_ids: List[str] = [uid.strip() for uid in user_ids_csv.split(',') if uid and uid.strip()]
|
|
421
|
-
if not user_ids:
|
|
422
|
-
write_log_message(f"알림 저장 생략: 유효한 사용자 ID 없음 (user_ids_csv={user_ids_csv})")
|
|
423
|
-
return
|
|
424
|
-
|
|
425
|
-
rows: List[Dict[str, Any]] = []
|
|
426
|
-
for uid in user_ids:
|
|
427
|
-
rows.append(
|
|
428
|
-
{
|
|
429
|
-
"id": str(uuid.uuid4()), # UUID 자동 생성
|
|
430
|
-
"user_id": uid,
|
|
431
|
-
"tenant_id": tenant_id,
|
|
432
|
-
"title": title,
|
|
433
|
-
"description": description,
|
|
434
|
-
"type": notif_type,
|
|
435
|
-
"url": url,
|
|
436
|
-
"from_user_id": from_user_id,
|
|
437
|
-
}
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
supabase.table("notifications").insert(rows).execute()
|
|
441
|
-
write_log_message(f"알림 저장 완료: {len(rows)}건")
|
|
442
|
-
except Exception as e:
|
|
443
|
-
handle_application_error("알림저장오류", e, raise_error=False)
|
|
444
|
-
|
|
445
|
-
# ============================================================================
|
|
446
|
-
# 상태 변경
|
|
447
|
-
# 설명: 실패 작업 상태 업데이트
|
|
448
|
-
# ============================================================================
|
|
449
|
-
|
|
450
|
-
async def update_task_error(todo_id: str) -> None:
|
|
451
|
-
"""실패 작업의 상태를 FAILED로 갱신한다."""
|
|
452
|
-
if not todo_id:
|
|
453
|
-
return
|
|
454
|
-
def _call():
|
|
455
|
-
client = get_db_client()
|
|
456
|
-
return (
|
|
457
|
-
client
|
|
458
|
-
.table('todolist')
|
|
459
|
-
.update({'draft_status': 'FAILED', 'consumer': None})
|
|
460
|
-
.eq('id', todo_id)
|
|
461
|
-
.execute()
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
await _async_retry(_call, name="update_task_error", fallback=lambda: None)
|