process-gpt-agent-sdk 0.3.10__py3-none-any.whl → 0.3.12__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.12.dist-info/METADATA +410 -0
- process_gpt_agent_sdk-0.3.12.dist-info/RECORD +6 -0
- processgpt_agent_sdk/database.py +537 -0
- processgpt_agent_sdk/processgpt_agent_framework.py +1 -1
- process_gpt_agent_sdk-0.3.10.dist-info/METADATA +0 -336
- process_gpt_agent_sdk-0.3.10.dist-info/RECORD +0 -5
- {process_gpt_agent_sdk-0.3.10.dist-info → process_gpt_agent_sdk-0.3.12.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.3.10.dist-info → process_gpt_agent_sdk-0.3.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: process-gpt-agent-sdk
|
|
3
|
+
Version: 0.3.12
|
|
4
|
+
Summary: Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/your-org/process-gpt-agent-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/your-org/process-gpt-agent-sdk/issues
|
|
8
|
+
Keywords: agent,a2a,supabase,workflow,sdk,processgpt
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: supabase>=2.0.0
|
|
16
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
17
|
+
Requires-Dist: click>=8.0.0
|
|
18
|
+
Requires-Dist: asyncio-mqtt>=0.13.0
|
|
19
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
20
|
+
Requires-Dist: structlog>=23.0.0
|
|
21
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
22
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
23
|
+
Requires-Dist: a2a-sdk==0.3.0
|
|
24
|
+
|
|
25
|
+
# ProcessGPT Agent Framework
|
|
26
|
+
## A2A SDK 연동을 위한 경량 에이전트 서버 프레임워크
|
|
27
|
+
|
|
28
|
+
Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이벤트를 통해 작업 상태/결과를 기록하는 **경량 에이전트 서버 프레임워크**입니다.
|
|
29
|
+
|
|
30
|
+
### 📋 요구사항
|
|
31
|
+
- **런타임**: Python 3.9+ (권장: Python 3.11)
|
|
32
|
+
- **데이터베이스**: Supabase (PostgreSQL) + 제공된 RPC/테이블
|
|
33
|
+
- **이벤트 규격**: A2A `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent`
|
|
34
|
+
|
|
35
|
+
## 📊 이벤트 타입별 저장 테이블 및 특징
|
|
36
|
+
|
|
37
|
+
### 1. TaskStatusUpdateEvent (작업 상태 이벤트)
|
|
38
|
+
- **저장 테이블**: `events`
|
|
39
|
+
- **용도**: 작업 진행 상황, 사용자 입력 요청, 에러 알림 등
|
|
40
|
+
- **저장 데이터**: 메시지 래퍼를 제거한 순수 payload만 `data` 컬럼에 JSON으로 저장
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# 예시 코드
|
|
44
|
+
event_queue.enqueue_event(
|
|
45
|
+
TaskStatusUpdateEvent(
|
|
46
|
+
status={
|
|
47
|
+
"state": TaskState.working, # working, input_required, completed 등
|
|
48
|
+
"message": new_agent_text_message("진행 중입니다", context_id, task_id),
|
|
49
|
+
},
|
|
50
|
+
final=False,
|
|
51
|
+
contextId=context_id,
|
|
52
|
+
taskId=task_id,
|
|
53
|
+
metadata={"event_type": "task_started"} # events.event_type에 저장
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**특별 규칙**:
|
|
59
|
+
- `state=input_required`일 때는 자동으로 `event_type=human_asked`로 저장됨
|
|
60
|
+
- 메시지는 `new_agent_text_message()` 유틸 함수로 생성
|
|
61
|
+
|
|
62
|
+
### 2. TaskArtifactUpdateEvent (작업 결과 이벤트)
|
|
63
|
+
- **저장 테이블**: `todolist` (output 컬럼)
|
|
64
|
+
- **용도**: 최종 작업 결과물 전송
|
|
65
|
+
- **저장 데이터**: 아티팩트 래퍼를 제거한 순수 payload만 `output` 컬럼에 JSON으로 저장
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# 예시 코드
|
|
69
|
+
artifact = new_text_artifact(
|
|
70
|
+
name="처리결과",
|
|
71
|
+
description="작업 완료 결과",
|
|
72
|
+
text="실제 결과 데이터"
|
|
73
|
+
)
|
|
74
|
+
event_queue.enqueue_event(
|
|
75
|
+
TaskArtifactUpdateEvent(
|
|
76
|
+
artifact=artifact,
|
|
77
|
+
lastChunk=True, # 최종 결과면 True
|
|
78
|
+
contextId=context_id,
|
|
79
|
+
taskId=task_id,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**특별 규칙**:
|
|
85
|
+
- `lastChunk=True` 또는 `final=True`일 때만 최종 저장됨 (`p_final=true`)
|
|
86
|
+
- 아티팩트는 `new_text_artifact()` 유틸 함수로 생성
|
|
87
|
+
|
|
88
|
+
## 🔄 데이터 흐름과 값 전달 방식
|
|
89
|
+
|
|
90
|
+
### 전체 흐름
|
|
91
|
+
1. **작업 폴링**: 서버가 Supabase `todolist` 테이블에서 새 작업을 가져옴
|
|
92
|
+
2. **컨텍스트 준비**: `RequestContext`에 작업 정보와 사용자 입력을 담음
|
|
93
|
+
3. **익스큐터 실행**: 사용자가 구현한 `AgentExecutor.execute()` 메서드 호출
|
|
94
|
+
4. **이벤트 전송**: 익스큐터에서 진행 상황과 결과를 이벤트로 전송
|
|
95
|
+
5. **데이터 저장**: 이벤트 타입에 따라 적절한 테이블에 저장
|
|
96
|
+
|
|
97
|
+
### 값 전달 과정
|
|
98
|
+
```python
|
|
99
|
+
# 1. 서버에서 작업 정보 가져오기
|
|
100
|
+
row = context.get_context_data()["row"] # todolist 테이블의 한 행
|
|
101
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id") # 프로세스 ID
|
|
102
|
+
task_id = row.get("id") # 작업 ID
|
|
103
|
+
user_input = context.get_user_input() # 사용자가 입력한 내용
|
|
104
|
+
|
|
105
|
+
# 2. 메시지/아티팩트 생성시 JSON 문자열로 변환
|
|
106
|
+
payload = {"result": "처리 완료"}
|
|
107
|
+
message_text = json.dumps(payload, ensure_ascii=False) # 중요: JSON 문자열로!
|
|
108
|
+
|
|
109
|
+
# 3. 서버가 자동으로 래퍼 제거 후 순수 payload만 저장
|
|
110
|
+
# events.data 또는 todolist.output에 {"result": "처리 완료"}만 저장됨
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 🚀 빠른 시작 가이드
|
|
114
|
+
|
|
115
|
+
### 1단계: 설치
|
|
116
|
+
```bash
|
|
117
|
+
# 패키지 설치
|
|
118
|
+
pip install -e .
|
|
119
|
+
|
|
120
|
+
# 또는 requirements.txt 사용
|
|
121
|
+
pip install -r requirements.txt
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2단계: 환경 설정
|
|
125
|
+
`.env` 파일 생성:
|
|
126
|
+
```env
|
|
127
|
+
SUPABASE_URL=your_supabase_project_url
|
|
128
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
129
|
+
ENV=dev
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3단계: 서버 구현 방법
|
|
133
|
+
서버는 이렇게 만드세요:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# my_server.py
|
|
137
|
+
import asyncio
|
|
138
|
+
from dotenv import load_dotenv
|
|
139
|
+
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
140
|
+
from my_executor import MyExecutor # 아래에서 구현할 익스큐터
|
|
141
|
+
|
|
142
|
+
async def main():
|
|
143
|
+
load_dotenv()
|
|
144
|
+
|
|
145
|
+
server = ProcessGPTAgentServer(
|
|
146
|
+
agent_executor=MyExecutor(), # 여러분이 구현할 익스큐터
|
|
147
|
+
agent_type="my-agent" # Supabase todolist.agent_orch와 매칭되어야 함
|
|
148
|
+
)
|
|
149
|
+
server.polling_interval = 3 # 3초마다 새 작업 확인
|
|
150
|
+
|
|
151
|
+
print("서버 시작!")
|
|
152
|
+
await server.run()
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
try:
|
|
156
|
+
asyncio.run(main())
|
|
157
|
+
except KeyboardInterrupt:
|
|
158
|
+
print("서버 종료")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 4단계: 익스큐터 구현 방법
|
|
162
|
+
익스큐터는 이렇게 만드세요:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# my_executor.py
|
|
166
|
+
import asyncio
|
|
167
|
+
import json
|
|
168
|
+
from typing_extensions import override
|
|
169
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
170
|
+
from a2a.server.events import EventQueue
|
|
171
|
+
from a2a.types import TaskStatusUpdateEvent, TaskState, TaskArtifactUpdateEvent
|
|
172
|
+
from a2a.utils import new_agent_text_message, new_text_artifact
|
|
173
|
+
|
|
174
|
+
class MyExecutor(AgentExecutor):
|
|
175
|
+
@override
|
|
176
|
+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
177
|
+
# 1. 작업 정보 가져오기
|
|
178
|
+
row = context.get_context_data()["row"]
|
|
179
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id")
|
|
180
|
+
task_id = row.get("id")
|
|
181
|
+
user_input = context.get_user_input() # 사용자가 입력한 내용
|
|
182
|
+
|
|
183
|
+
print(f"처리할 작업: {user_input}")
|
|
184
|
+
|
|
185
|
+
# 2. 작업 시작 알림 (events 테이블에 저장됨)
|
|
186
|
+
event_queue.enqueue_event(
|
|
187
|
+
TaskStatusUpdateEvent(
|
|
188
|
+
status={
|
|
189
|
+
"state": TaskState.working,
|
|
190
|
+
"message": new_agent_text_message("작업 시작", context_id, task_id),
|
|
191
|
+
},
|
|
192
|
+
final=False,
|
|
193
|
+
contextId=context_id,
|
|
194
|
+
taskId=task_id,
|
|
195
|
+
metadata={"event_type": "task_started"}
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# 3. 실제 작업 수행 (여기에 여러분의 로직 작성)
|
|
200
|
+
await asyncio.sleep(2)
|
|
201
|
+
result_data = {"status": "완료", "input": user_input, "output": "처리 결과"}
|
|
202
|
+
|
|
203
|
+
# 4. 최종 결과 전송 (todolist.output에 저장됨)
|
|
204
|
+
artifact = new_text_artifact(
|
|
205
|
+
name="처리결과",
|
|
206
|
+
description="작업 완료 결과",
|
|
207
|
+
text=json.dumps(result_data, ensure_ascii=False) # JSON 문자열로!
|
|
208
|
+
)
|
|
209
|
+
event_queue.enqueue_event(
|
|
210
|
+
TaskArtifactUpdateEvent(
|
|
211
|
+
artifact=artifact,
|
|
212
|
+
lastChunk=True, # 중요: 최종 결과면 True
|
|
213
|
+
contextId=context_id,
|
|
214
|
+
taskId=task_id,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
220
|
+
pass # 취소 로직 (필요시 구현)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 5단계: 실행
|
|
224
|
+
```bash
|
|
225
|
+
python my_server.py
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 🤝 Human-in-the-Loop (사용자 입력 요청) 패턴
|
|
229
|
+
|
|
230
|
+
사용자 입력이 필요할 때:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# 사용자 입력 요청
|
|
234
|
+
question_data = {
|
|
235
|
+
"question": "어떤 방식으로 처리할까요?",
|
|
236
|
+
"options": ["방식A", "방식B", "방식C"]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
event_queue.enqueue_event(
|
|
240
|
+
TaskStatusUpdateEvent(
|
|
241
|
+
status={
|
|
242
|
+
"state": TaskState.input_required, # 이 상태가 중요!
|
|
243
|
+
"message": new_agent_text_message(
|
|
244
|
+
json.dumps(question_data, ensure_ascii=False),
|
|
245
|
+
context_id, task_id
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
final=True,
|
|
249
|
+
contextId=context_id,
|
|
250
|
+
taskId=task_id,
|
|
251
|
+
metadata={"job_id": f"job-{task_id}"} # job_id 필수
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
# 자동으로 events 테이블에 event_type=human_asked로 저장됨
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 📋 체크리스트 (실패 없는 통합을 위한)
|
|
258
|
+
|
|
259
|
+
### 필수 설정
|
|
260
|
+
- [ ] `.env`에 `SUPABASE_URL`, `SUPABASE_KEY` 설정
|
|
261
|
+
- [ ] `requirements.txt` 설치 완료
|
|
262
|
+
- [ ] Supabase에서 제공 SQL(`database_schema.sql`, `function.sql`) 적용
|
|
263
|
+
|
|
264
|
+
### 코드 구현
|
|
265
|
+
- [ ] 서버에서 `agent_type`이 Supabase `todolist.agent_orch`와 매칭됨
|
|
266
|
+
- [ ] 익스큐터에서 `contextId`, `taskId`를 올바르게 설정
|
|
267
|
+
- [ ] 상태 이벤트는 `new_agent_text_message()`로 생성
|
|
268
|
+
- [ ] 최종 결과는 `new_text_artifact()` + `lastChunk=True`로 전송
|
|
269
|
+
- [ ] HITL 요청시 `TaskState.input_required` 사용
|
|
270
|
+
|
|
271
|
+
## 🚨 자주 발생하는 문제
|
|
272
|
+
|
|
273
|
+
### 1. 설치 문제
|
|
274
|
+
**증상**: `ModuleNotFoundError`
|
|
275
|
+
```bash
|
|
276
|
+
# 해결
|
|
277
|
+
pip install -e .
|
|
278
|
+
pip install a2a-sdk==0.3.0 --force-reinstall
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 2. 작업이 폴링되지 않음
|
|
282
|
+
**원인**: Supabase 연결 문제
|
|
283
|
+
**해결**:
|
|
284
|
+
- `.env` 파일 위치 확인 (프로젝트 루트)
|
|
285
|
+
- URL/Key 재확인
|
|
286
|
+
- `agent_type`이 todolist.agent_orch와 매칭되는지 확인
|
|
287
|
+
|
|
288
|
+
### 3. 이벤트가 저장되지 않음
|
|
289
|
+
**원인**: 테이블/함수 누락
|
|
290
|
+
**해결**:
|
|
291
|
+
- `database_schema.sql`, `function.sql` 실행 확인
|
|
292
|
+
- Supabase 테이블 권한 확인
|
|
293
|
+
|
|
294
|
+
### 4. 결과가 래퍼와 함께 저장됨
|
|
295
|
+
**원인**: JSON 문자열 변환 누락
|
|
296
|
+
```python
|
|
297
|
+
# 올바른 방법
|
|
298
|
+
text=json.dumps(data, ensure_ascii=False) # JSON 문자열로!
|
|
299
|
+
|
|
300
|
+
# 잘못된 방법
|
|
301
|
+
text=data # 딕셔너리 직접 전달 (X)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## 📚 샘플 코드 (간단 버전)
|
|
305
|
+
|
|
306
|
+
### 기본 서버
|
|
307
|
+
```python
|
|
308
|
+
# sample_server/minimal_server.py
|
|
309
|
+
import asyncio
|
|
310
|
+
from dotenv import load_dotenv
|
|
311
|
+
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
312
|
+
from sample_server.minimal_executor import MinimalExecutor
|
|
313
|
+
|
|
314
|
+
async def main():
|
|
315
|
+
load_dotenv()
|
|
316
|
+
server = ProcessGPTAgentServer(
|
|
317
|
+
agent_executor=MinimalExecutor(),
|
|
318
|
+
agent_type="crewai-action"
|
|
319
|
+
)
|
|
320
|
+
server.polling_interval = 3
|
|
321
|
+
await server.run()
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
try:
|
|
325
|
+
asyncio.run(main())
|
|
326
|
+
except KeyboardInterrupt:
|
|
327
|
+
pass
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 기본 익스큐터
|
|
331
|
+
```python
|
|
332
|
+
# sample_server/minimal_executor.py
|
|
333
|
+
import asyncio
|
|
334
|
+
import json
|
|
335
|
+
from typing_extensions import override
|
|
336
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
337
|
+
from a2a.server.events import EventQueue
|
|
338
|
+
from a2a.types import TaskStatusUpdateEvent, TaskState, TaskArtifactUpdateEvent
|
|
339
|
+
from a2a.utils import new_agent_text_message, new_text_artifact
|
|
340
|
+
|
|
341
|
+
class MinimalExecutor(AgentExecutor):
|
|
342
|
+
@override
|
|
343
|
+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
344
|
+
row = context.get_context_data()["row"]
|
|
345
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id")
|
|
346
|
+
task_id = row.get("id")
|
|
347
|
+
user_input = context.get_user_input()
|
|
348
|
+
|
|
349
|
+
# 진행 상태
|
|
350
|
+
event_queue.enqueue_event(
|
|
351
|
+
TaskStatusUpdateEvent(
|
|
352
|
+
status={
|
|
353
|
+
"state": TaskState.working,
|
|
354
|
+
"message": new_agent_text_message("처리중", context_id, task_id),
|
|
355
|
+
},
|
|
356
|
+
final=False,
|
|
357
|
+
contextId=context_id,
|
|
358
|
+
taskId=task_id,
|
|
359
|
+
metadata={"event_type": "task_started"}
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
await asyncio.sleep(1)
|
|
364
|
+
|
|
365
|
+
# 최종 결과
|
|
366
|
+
result = {"input": user_input, "output": "처리 완료"}
|
|
367
|
+
artifact = new_text_artifact(
|
|
368
|
+
name="결과",
|
|
369
|
+
description="처리 결과",
|
|
370
|
+
text=json.dumps(result, ensure_ascii=False)
|
|
371
|
+
)
|
|
372
|
+
event_queue.enqueue_event(
|
|
373
|
+
TaskArtifactUpdateEvent(
|
|
374
|
+
artifact=artifact,
|
|
375
|
+
lastChunk=True,
|
|
376
|
+
contextId=context_id,
|
|
377
|
+
taskId=task_id,
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@override
|
|
382
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
383
|
+
pass
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## 🔧 실행 방법
|
|
387
|
+
|
|
388
|
+
### 개발 환경에서 실행
|
|
389
|
+
```bash
|
|
390
|
+
python sample_server/minimal_server.py
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 실제 사용시
|
|
394
|
+
```bash
|
|
395
|
+
python my_server.py
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 📚 레퍼런스
|
|
401
|
+
|
|
402
|
+
### 주요 함수들
|
|
403
|
+
- `ProcessGPTAgentServer.run()`: 서버 시작
|
|
404
|
+
- `new_agent_text_message(text, context_id, task_id)`: 상태 메시지 생성
|
|
405
|
+
- `new_text_artifact(name, desc, text)`: 결과 아티팩트 생성
|
|
406
|
+
|
|
407
|
+
### 이벤트 저장 규칙
|
|
408
|
+
- **TaskStatusUpdateEvent** → `events` 테이블 (`data` 컬럼)
|
|
409
|
+
- **TaskArtifactUpdateEvent** → `todolist` 테이블 (`output` 컬럼)
|
|
410
|
+
- 래퍼 자동 제거 후 순수 payload만 저장
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
processgpt_agent_sdk/database.py,sha256=k219S54XwQoGJcKYq6Zr0f7j-Dtrp-v7SpLMRnIgHCI,18979
|
|
2
|
+
processgpt_agent_sdk/processgpt_agent_framework.py,sha256=YE5JB4TyRkYFYxRMRfo-SyXb5JuFZwaNtNaxFNd9PXA,16894
|
|
3
|
+
process_gpt_agent_sdk-0.3.12.dist-info/METADATA,sha256=x5kNRsZ9J0jaCmYmWEhQiuDeiXqH_vgjaDkZ8jizLSE,13600
|
|
4
|
+
process_gpt_agent_sdk-0.3.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
process_gpt_agent_sdk-0.3.12.dist-info/top_level.txt,sha256=Xe6zrj3_3Vv7d0pl5RRtenVUckwOVBVLQn2P03j5REo,21
|
|
6
|
+
process_gpt_agent_sdk-0.3.12.dist-info/RECORD,,
|
|
@@ -0,0 +1,537 @@
|
|
|
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
|
+
|
|
16
|
+
# 모듈 전역 로거 (정상 경로는 로깅하지 않고, 오류 시에만 사용)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# Utility: 재시도 헬퍼 및 유틸
|
|
22
|
+
# 설명: 동기 DB 호출을 안전하게 재시도 (지수 백오프 + 지터) 및 유틸
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
async def _async_retry(
|
|
26
|
+
fn: Callable[[], T],
|
|
27
|
+
*,
|
|
28
|
+
name: str,
|
|
29
|
+
retries: int = 3,
|
|
30
|
+
base_delay: float = 0.8,
|
|
31
|
+
fallback: Optional[Callable[[], T]] = None,
|
|
32
|
+
) -> Optional[T]:
|
|
33
|
+
"""지수 백오프+jitter로 재시도하고 실패 시 fallback/None 반환."""
|
|
34
|
+
last_err: Optional[Exception] = None
|
|
35
|
+
for attempt in range(1, retries + 1):
|
|
36
|
+
try:
|
|
37
|
+
return await asyncio.to_thread(fn)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
last_err = e
|
|
40
|
+
jitter = random.uniform(0, 0.3)
|
|
41
|
+
delay = base_delay * (2 ** (attempt - 1)) + jitter
|
|
42
|
+
await asyncio.sleep(delay)
|
|
43
|
+
if last_err is not None:
|
|
44
|
+
logger.error(
|
|
45
|
+
"retry failed: name=%s retries=%s error=%s", name, retries, str(last_err),
|
|
46
|
+
exc_info=last_err,
|
|
47
|
+
)
|
|
48
|
+
if fallback is not None:
|
|
49
|
+
try:
|
|
50
|
+
fb_val = fallback()
|
|
51
|
+
return fb_val
|
|
52
|
+
except Exception as fb_err:
|
|
53
|
+
logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
|
|
54
|
+
return None
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
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
|
+
def _to_jsonable(value: Any) -> Any:
|
|
67
|
+
"""간단한 JSON 변환: dict 재귀, list/tuple/set→list, 기본형 유지, 나머지는 repr."""
|
|
68
|
+
try:
|
|
69
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
70
|
+
return value
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
73
|
+
if isinstance(value, (list, tuple, set)):
|
|
74
|
+
return [_to_jsonable(v) for v in list(value)]
|
|
75
|
+
if hasattr(value, "__dict__"):
|
|
76
|
+
return _to_jsonable(vars(value))
|
|
77
|
+
return repr(value)
|
|
78
|
+
except Exception:
|
|
79
|
+
return repr(value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# DB 연결/클라이언트
|
|
84
|
+
# 설명: 환경 변수 로드, Supabase 클라이언트 초기화/반환, 컨슈머 식별자
|
|
85
|
+
# ============================================================================
|
|
86
|
+
_supabase_client: Optional[Client] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def initialize_db() -> None:
|
|
90
|
+
"""환경변수 로드 및 Supabase 클라이언트 초기화"""
|
|
91
|
+
global _supabase_client
|
|
92
|
+
if _supabase_client is not None:
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
if os.getenv("ENV") != "production":
|
|
96
|
+
load_dotenv()
|
|
97
|
+
supabase_url = os.getenv("SUPABASE_URL") or os.getenv("SUPABASE_KEY_URL")
|
|
98
|
+
supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
|
|
99
|
+
if not supabase_url or not supabase_key:
|
|
100
|
+
raise RuntimeError("SUPABASE_URL 및 SUPABASE_KEY가 필요합니다")
|
|
101
|
+
_supabase_client = create_client(supabase_url, supabase_key)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error("initialize_db failed: %s", str(e), exc_info=e)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_db_client() -> Client:
|
|
108
|
+
"""초기화된 Supabase 클라이언트 반환."""
|
|
109
|
+
if _supabase_client is None:
|
|
110
|
+
raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
|
|
111
|
+
return _supabase_client
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_consumer_id() -> str:
|
|
115
|
+
"""파드/프로세스 식별자 생성(CONSUMER_ID>HOST:PID)."""
|
|
116
|
+
env_consumer = os.getenv("CONSUMER_ID")
|
|
117
|
+
if env_consumer:
|
|
118
|
+
return env_consumer
|
|
119
|
+
host = socket.gethostname()
|
|
120
|
+
pid = os.getpid()
|
|
121
|
+
return f"{host}:{pid}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ============================================================================
|
|
125
|
+
# 데이터 조회
|
|
126
|
+
# 설명: TODOLIST 테이블 조회, 완료 output 목록 조회, 이벤트 조회, 폼 조회, 테넌트 MCP 설정 조회, 사용자 및 에이전트 조회
|
|
127
|
+
# ============================================================================
|
|
128
|
+
async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
|
|
129
|
+
"""TODOLIST 테이블에서 대기중인 워크아이템을 조회 (agent_orch 전달).
|
|
130
|
+
|
|
131
|
+
- 정상 동작 시 로그를 남기지 않는다.
|
|
132
|
+
- 예외 시에만 풍부한 에러 정보를 남기되, 호출자에게 None을 반환하여 폴링 루프가 중단되지 않게 한다.
|
|
133
|
+
"""
|
|
134
|
+
if agent_orch is None:
|
|
135
|
+
agent_orch = ""
|
|
136
|
+
if consumer is None:
|
|
137
|
+
consumer = ""
|
|
138
|
+
|
|
139
|
+
def _call():
|
|
140
|
+
client = get_db_client()
|
|
141
|
+
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
|
+
|
|
160
|
+
|
|
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
|
+
rows = resp.data or []
|
|
176
|
+
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
|
+
|
|
182
|
+
async def fetch_task_status(todo_id: str) -> Optional[str]:
|
|
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")
|
|
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
|
+
|
|
256
|
+
def _call():
|
|
257
|
+
client = get_db_client()
|
|
258
|
+
resp = (
|
|
259
|
+
client
|
|
260
|
+
.table("users")
|
|
261
|
+
.select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
|
|
262
|
+
.in_("id", valid_ids)
|
|
263
|
+
.eq("is_agent", True)
|
|
264
|
+
.execute()
|
|
265
|
+
)
|
|
266
|
+
rows = resp.data or []
|
|
267
|
+
normalized: List[Dict[str, Any]] = []
|
|
268
|
+
for row in rows:
|
|
269
|
+
normalized.append(
|
|
270
|
+
{
|
|
271
|
+
"id": row.get("id"),
|
|
272
|
+
"name": row.get("username"),
|
|
273
|
+
"role": row.get("role"),
|
|
274
|
+
"goal": row.get("goal"),
|
|
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
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
result = await _async_retry(_call, name="fetch_agent_data", fallback=lambda: [])
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error("fetch_agent_data fatal: %s", str(e), exc_info=e)
|
|
289
|
+
return await fetch_all_agents()
|
|
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
|
|
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
|
+
|
|
332
|
+
|
|
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
|
+
def _call():
|
|
338
|
+
client = get_db_client()
|
|
339
|
+
return client.table("tenants").select("mcp").eq("id", tenant_id).single().execute()
|
|
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)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ============================================================================
|
|
410
|
+
# 데이터 저장
|
|
411
|
+
# 설명: 이벤트/알림/작업 결과 저장
|
|
412
|
+
# ============================================================================
|
|
413
|
+
async def record_event(payload: Dict[str, Any]) -> None:
|
|
414
|
+
"""UI용 events 테이블에 이벤트 기록 (전달된 payload 그대로 저장)"""
|
|
415
|
+
if payload is None:
|
|
416
|
+
logger.error("record_event invalid payload: None")
|
|
417
|
+
return
|
|
418
|
+
def _call():
|
|
419
|
+
client = get_db_client()
|
|
420
|
+
safe_payload = _to_jsonable(payload)
|
|
421
|
+
# 상태값이 빈 문자열이면 NULL로
|
|
422
|
+
if isinstance(safe_payload, dict):
|
|
423
|
+
status_val = safe_payload.get("status")
|
|
424
|
+
if status_val == "":
|
|
425
|
+
safe_payload["status"] = None
|
|
426
|
+
return client.table("events").insert(safe_payload).execute()
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
resp = await _async_retry(_call, name="record_event", fallback=lambda: None)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
try:
|
|
432
|
+
logger.error("record_event fatal: %s payload=%s", str(e), json.dumps(_to_jsonable(payload), ensure_ascii=False), exc_info=e)
|
|
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
|
+
|
|
443
|
+
|
|
444
|
+
async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
|
|
445
|
+
"""작업 결과를 저장한다. final=True 시 최종 저장."""
|
|
446
|
+
if not todo_id:
|
|
447
|
+
logger.error("save_task_result invalid todo_id: %s", str(todo_id))
|
|
448
|
+
return
|
|
449
|
+
# 안전한 직렬화: 실패 시 문자열화하여 저장자가 원인 파악 가능
|
|
450
|
+
def _safe_payload(val: Any) -> Any:
|
|
451
|
+
try:
|
|
452
|
+
return _to_jsonable(val)
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.error("save_task_result payload serialization failed: %s", str(e), exc_info=e)
|
|
455
|
+
try:
|
|
456
|
+
return {"repr": repr(val)}
|
|
457
|
+
except Exception:
|
|
458
|
+
return {"error": "unserializable payload"}
|
|
459
|
+
|
|
460
|
+
def _call():
|
|
461
|
+
client = get_db_client()
|
|
462
|
+
payload = _safe_payload(result)
|
|
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)
|
|
514
|
+
|
|
515
|
+
# ============================================================================
|
|
516
|
+
# 상태 변경
|
|
517
|
+
# 설명: 실패 작업 상태 업데이트
|
|
518
|
+
# ============================================================================
|
|
519
|
+
|
|
520
|
+
async def update_task_error(todo_id: str) -> None:
|
|
521
|
+
"""실패 작업의 상태를 FAILED로 갱신한다."""
|
|
522
|
+
if not todo_id:
|
|
523
|
+
return
|
|
524
|
+
def _call():
|
|
525
|
+
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
|
+
)
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
await _async_retry(_call, name="update_task_error", fallback=lambda: None)
|
|
536
|
+
except Exception as e:
|
|
537
|
+
logger.error("update_task_error fatal: %s", str(e), exc_info=e)
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: process-gpt-agent-sdk
|
|
3
|
-
Version: 0.3.10
|
|
4
|
-
Summary: Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK
|
|
5
|
-
License: MIT
|
|
6
|
-
Project-URL: Homepage, https://github.com/your-org/process-gpt-agent-sdk
|
|
7
|
-
Project-URL: Issues, https://github.com/your-org/process-gpt-agent-sdk/issues
|
|
8
|
-
Keywords: agent,a2a,supabase,workflow,sdk,processgpt
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Requires-Python: >=3.9
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: supabase>=2.0.0
|
|
16
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
17
|
-
Requires-Dist: click>=8.0.0
|
|
18
|
-
Requires-Dist: asyncio-mqtt>=0.13.0
|
|
19
|
-
Requires-Dist: jsonschema>=4.0.0
|
|
20
|
-
Requires-Dist: structlog>=23.0.0
|
|
21
|
-
Requires-Dist: typing-extensions>=4.0.0
|
|
22
|
-
Requires-Dist: python-dateutil>=2.8.0
|
|
23
|
-
Requires-Dist: a2a-sdk==0.3.0
|
|
24
|
-
|
|
25
|
-
# ProcessGPT Agent Framework (A2A SDK 연동 가이드)
|
|
26
|
-
|
|
27
|
-
이 저장소는 Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이벤트를 통해 작업 상태/결과를 기록하는 **경량 에이전트 서버 프레임워크**입니다. 최소 구현으로 빠르게 통합하고, 필요하면 커스터마이즈할 수 있습니다.
|
|
28
|
-
|
|
29
|
-
- 런타임: Python 3.10+
|
|
30
|
-
- 저장소 의존: Supabase(Postgres) + 제공된 RPC/테이블
|
|
31
|
-
- 이벤트 규격: A2A `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## 아키텍처 한눈에 보기
|
|
35
|
-
```mermaid
|
|
36
|
-
flowchart LR
|
|
37
|
-
subgraph Supabase
|
|
38
|
-
A[Todolist] --- B[Events]
|
|
39
|
-
A -.RPC.-> C[(save_task_result)]
|
|
40
|
-
D[(fetch_pending_task)] --> A
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
subgraph Agent Server
|
|
44
|
-
E[ProcessGPTAgentServer] -->|polls| D
|
|
45
|
-
E --> F[ProcessGPTRequestContext]
|
|
46
|
-
E --> G[ProcessGPTEventQueue]
|
|
47
|
-
H[Your AgentExecutor]
|
|
48
|
-
F --> H
|
|
49
|
-
H -->|A2A Events| G
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
G -->|TaskStatusUpdateEvent| B
|
|
53
|
-
G -->|TaskArtifactUpdateEvent| A
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
- 서버는 주기적으로 Todolist를 폴링하여 새 작업을 가져옵니다.
|
|
57
|
-
- 사용자 구현 `AgentExecutor`가 요청을 처리하고, A2A 이벤트를 큐에 전달합니다.
|
|
58
|
-
- 이벤트 큐는 상태 이벤트를 `events` 테이블에, 아티팩트 이벤트를 `todolist.output`에 저장합니다.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
## 엔드-투-엔드 시퀀스(정상 흐름)
|
|
62
|
-
```mermaid
|
|
63
|
-
sequenceDiagram
|
|
64
|
-
participant SB as Supabase
|
|
65
|
-
participant SRV as ProcessGPTAgentServer
|
|
66
|
-
participant CTX as RequestContext
|
|
67
|
-
participant EXE as Your AgentExecutor
|
|
68
|
-
participant EQ as ProcessGPTEventQueue
|
|
69
|
-
|
|
70
|
-
SRV->>SB: RPC fetch_pending_task
|
|
71
|
-
SB-->>SRV: todolist row
|
|
72
|
-
SRV->>CTX: prepare_context()
|
|
73
|
-
SRV->>EXE: execute(context, event_queue)
|
|
74
|
-
EXE->>EQ: TaskStatusUpdateEvent (state=working)
|
|
75
|
-
EQ->>SB: INSERT events (data=payload)
|
|
76
|
-
EXE->>EQ: TaskArtifactUpdateEvent (lastChunk=true, artifact)
|
|
77
|
-
EQ->>SB: RPC save_task_result (output=payload, p_final=true)
|
|
78
|
-
SRV->>EQ: task_done()
|
|
79
|
-
EQ->>SB: INSERT events (crew_completed)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Human-in-the-loop(HITL) 시퀀스
|
|
84
|
-
```mermaid
|
|
85
|
-
sequenceDiagram
|
|
86
|
-
participant EXE as Your AgentExecutor
|
|
87
|
-
participant EQ as ProcessGPTEventQueue
|
|
88
|
-
participant SB as Supabase
|
|
89
|
-
participant UI as Operator UI
|
|
90
|
-
|
|
91
|
-
EXE->>EQ: TaskStatusUpdateEvent (state=input_required)
|
|
92
|
-
Note right of EXE: event_type 전송 생략 가능
|
|
93
|
-
EQ->>SB: INSERT events (event_type=human_asked, data=질문 payload)
|
|
94
|
-
UI->>SB: INSERT events (event_type=human_response, data=사용자 응답)
|
|
95
|
-
EXE-->>SB: 선택: fetch_human_response_sync(job_id)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## 친절한 시작 가이드(5분 컷)
|
|
100
|
-
1) 가상환경 + 설치
|
|
101
|
-
```bash
|
|
102
|
-
uv venv --python 3.11.9
|
|
103
|
-
uv pip install -r requirements.txt
|
|
104
|
-
source .venv/Scripts/activate
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
2) .env 준비
|
|
108
|
-
- SUPABASE_URL, SUPABASE_KEY 필수
|
|
109
|
-
- ENV=dev (개발 환경에서 권장)
|
|
110
|
-
|
|
111
|
-
3) 샘플 서버 실행
|
|
112
|
-
```bash
|
|
113
|
-
python sample_server/minimal_server.py | cat
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
4) 이벤트 전송 패턴 이해
|
|
117
|
-
- 진행 상태: `TaskStatusUpdateEvent(state=working)` + `new_agent_text_message(text, contextId, taskId)`
|
|
118
|
-
- 사용자 입력 요청(HITL): `TaskState.input_required`만 보내면 event_type은 자동 `human_asked`
|
|
119
|
-
- 결과물: `TaskArtifactUpdateEvent(lastChunk=True)` + `new_text_artifact(name, desc, text)`
|
|
120
|
-
|
|
121
|
-
5) 저장물 확인 포인트
|
|
122
|
-
- `events` 테이블: data에는 래퍼 제거된 순수 payload 저장
|
|
123
|
-
- `todolist.output`: 순수 payload 저장, 최종 청크면 `p_final=true`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## 샘플 서버 실행 코드 (친절 버전)
|
|
127
|
-
### A. 가장 간단한 서버(minimal)
|
|
128
|
-
```python
|
|
129
|
-
# sample_server/minimal_server.py
|
|
130
|
-
import os
|
|
131
|
-
import sys
|
|
132
|
-
import asyncio
|
|
133
|
-
from dotenv import load_dotenv
|
|
134
|
-
|
|
135
|
-
# 패키지 루트 경로 추가 (샘플에서만)
|
|
136
|
-
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
|
137
|
-
|
|
138
|
-
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
139
|
-
from sample_server.minimal_executor import MinimalExecutor
|
|
140
|
-
|
|
141
|
-
async def main():
|
|
142
|
-
load_dotenv()
|
|
143
|
-
# agent_type은 Supabase의 todolist.agent_orch와 매칭되어야 함
|
|
144
|
-
server = ProcessGPTAgentServer(agent_executor=MinimalExecutor(), agent_type="crewai-action")
|
|
145
|
-
server.polling_interval = 3 # 초
|
|
146
|
-
await server.run()
|
|
147
|
-
|
|
148
|
-
if __name__ == "__main__":
|
|
149
|
-
try:
|
|
150
|
-
asyncio.run(main())
|
|
151
|
-
except KeyboardInterrupt:
|
|
152
|
-
pass
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
- Windows
|
|
156
|
-
```bash
|
|
157
|
-
python sample_server/minimal_server.py
|
|
158
|
-
```
|
|
159
|
-
- macOS/Linux
|
|
160
|
-
```bash
|
|
161
|
-
python3 sample_server/minimal_server.py
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### B. CLI 옵션이 있는 서버 예시
|
|
165
|
-
```python
|
|
166
|
-
# sample_server/crew_ai_dr_agent_server.py
|
|
167
|
-
import os
|
|
168
|
-
import sys
|
|
169
|
-
import asyncio
|
|
170
|
-
import click
|
|
171
|
-
from dotenv import load_dotenv
|
|
172
|
-
|
|
173
|
-
# 패키지 루트 경로 추가 (샘플에서만)
|
|
174
|
-
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
|
175
|
-
|
|
176
|
-
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
177
|
-
from sample_server.crew_ai_dr_agent_executor import CrewAIDeepResearchAgentExecutor
|
|
178
|
-
|
|
179
|
-
load_dotenv()
|
|
180
|
-
|
|
181
|
-
@click.command()
|
|
182
|
-
@click.option('--agent-type', default='crew-ai-dr', help='Agent type identifier')
|
|
183
|
-
@click.option('--polling-interval', default=5, help='Polling interval in seconds')
|
|
184
|
-
def cli_main(agent_type: str, polling_interval: int):
|
|
185
|
-
"""ProcessGPT Agent Server for CrewAI Deep Research Agent"""
|
|
186
|
-
|
|
187
|
-
agent_executor = CrewAIDeepResearchAgentExecutor()
|
|
188
|
-
server = ProcessGPTAgentServer(agent_executor=agent_executor, agent_type=agent_type)
|
|
189
|
-
server.polling_interval = polling_interval
|
|
190
|
-
|
|
191
|
-
print(f"Starting ProcessGPT Agent Server...")
|
|
192
|
-
print(f"Agent Type: {agent_type}")
|
|
193
|
-
print(f"Polling Interval: {polling_interval} seconds")
|
|
194
|
-
print("Press Ctrl+C to stop")
|
|
195
|
-
|
|
196
|
-
try:
|
|
197
|
-
asyncio.run(server.run())
|
|
198
|
-
except KeyboardInterrupt:
|
|
199
|
-
print("\nShutting down server...")
|
|
200
|
-
server.stop()
|
|
201
|
-
except Exception as e:
|
|
202
|
-
print(f"Server error: {e}")
|
|
203
|
-
sys.exit(1)
|
|
204
|
-
|
|
205
|
-
if __name__ == "__main__":
|
|
206
|
-
cli_main()
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
- 실행
|
|
210
|
-
- Windows
|
|
211
|
-
```bash
|
|
212
|
-
python sample_server/crew_ai_dr_agent_server.py --agent-type crew-ai-dr --polling-interval 3
|
|
213
|
-
```
|
|
214
|
-
- macOS/Linux
|
|
215
|
-
```bash
|
|
216
|
-
python3 sample_server/crew_ai_dr_agent_server.py --agent-type crew-ai-dr --polling-interval 3
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
## 최소 예시(익스큐터)
|
|
221
|
-
```python
|
|
222
|
-
# sample_server/minimal_executor.py (요약)
|
|
223
|
-
import asyncio
|
|
224
|
-
import json
|
|
225
|
-
from typing_extensions import override
|
|
226
|
-
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
227
|
-
from a2a.server.events import EventQueue
|
|
228
|
-
from a2a.types import TaskStatusUpdateEvent, TaskState, TaskArtifactUpdateEvent
|
|
229
|
-
from a2a.utils import new_agent_text_message, new_text_artifact
|
|
230
|
-
|
|
231
|
-
class MinimalExecutor(AgentExecutor):
|
|
232
|
-
@override
|
|
233
|
-
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
234
|
-
row = context.get_context_data()["row"]
|
|
235
|
-
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id")
|
|
236
|
-
task_id = row.get("id")
|
|
237
|
-
|
|
238
|
-
payload = {"order_process_activity_order_request_form": {"orderer_name": "안치윤","product_name": "금형세트","order_quantity": "50"}}
|
|
239
|
-
|
|
240
|
-
# 1) 진행 상태 이벤트
|
|
241
|
-
event_queue.enqueue_event(
|
|
242
|
-
TaskStatusUpdateEvent(
|
|
243
|
-
status={
|
|
244
|
-
"state": TaskState.working,
|
|
245
|
-
"message": new_agent_text_message(
|
|
246
|
-
json.dumps(payload, ensure_ascii=False),
|
|
247
|
-
context_id,
|
|
248
|
-
task_id,
|
|
249
|
-
),
|
|
250
|
-
},
|
|
251
|
-
final=False,
|
|
252
|
-
contextId=context_id,
|
|
253
|
-
taskId=task_id,
|
|
254
|
-
metadata={"crew_type": "action", "event_type": "task_started", "job_id": "job-demo-0001"},
|
|
255
|
-
)
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
await asyncio.sleep(0.1)
|
|
259
|
-
|
|
260
|
-
# 2) HITL: 사용자 입력 요청 (event_type 생략해도 자동 human_asked)
|
|
261
|
-
event_queue.enqueue_event(
|
|
262
|
-
TaskStatusUpdateEvent(
|
|
263
|
-
status={
|
|
264
|
-
"state": TaskState.input_required,
|
|
265
|
-
"message": new_agent_text_message(
|
|
266
|
-
json.dumps(payload, ensure_ascii=False),
|
|
267
|
-
context_id,
|
|
268
|
-
task_id,
|
|
269
|
-
),
|
|
270
|
-
},
|
|
271
|
-
final=True,
|
|
272
|
-
contextId=context_id,
|
|
273
|
-
taskId=task_id,
|
|
274
|
-
metadata={"crew_type": "action", "job_id": "job-demo-0001"},
|
|
275
|
-
)
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
await asyncio.sleep(0.1)
|
|
279
|
-
|
|
280
|
-
# 3) 최종 아티팩트
|
|
281
|
-
artifact = new_text_artifact(
|
|
282
|
-
name="current_result",
|
|
283
|
-
description="Result of request to agent.",
|
|
284
|
-
text=json.dumps(payload, ensure_ascii=False),
|
|
285
|
-
)
|
|
286
|
-
event_queue.enqueue_event(
|
|
287
|
-
TaskArtifactUpdateEvent(
|
|
288
|
-
artifact=artifact,
|
|
289
|
-
lastChunk=True,
|
|
290
|
-
contextId=context_id,
|
|
291
|
-
taskId=task_id,
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
## 서버가 해주는 일(정확한 규칙)
|
|
298
|
-
- 메시지/아티팩트 래퍼 제거 → `parts[0].text|content|data` → `root.*` → `top-level` 순서로 텍스트만 추출 후 JSON 파싱하여 저장
|
|
299
|
-
- `TaskStatusUpdateEvent` 수신 시
|
|
300
|
-
- `status.state == input_required`면 `event_type=human_asked`로 저장(명시값보다 우선)
|
|
301
|
-
- 그 외 상태는 `metadata.event_type` 저장(없으면 NULL)
|
|
302
|
-
- `TaskArtifactUpdateEvent` 수신 시
|
|
303
|
-
- `final` 또는 `lastChunk`가 참이면 최종 저장(`p_final=true`)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
## 체크리스트(실패 없는 통합을 위한)
|
|
307
|
-
- [ ] .env에 `SUPABASE_URL`, `SUPABASE_KEY` 설정했는가?
|
|
308
|
-
- [ ] `requirements.txt` 설치 완료했는가?
|
|
309
|
-
- [ ] Supabase에서 제공 SQL(`database_schema.sql`, `function.sql`) 적용했는가?
|
|
310
|
-
- [ ] 익스큐터에서 `contextId`, `taskId`를 todolist의 `proc_inst_id`, `id`로 매핑했는가?
|
|
311
|
-
- [ ] 상태 이벤트는 `new_agent_text_message`로 만들고 있는가?
|
|
312
|
-
- [ ] 최종 아티팩트는 `new_text_artifact` + `lastChunk=True`로 보내고 있는가?
|
|
313
|
-
- [ ] HITL 요청은 `TaskState.input_required`만 보내고 있는가?(event_type 생략 가능)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
## 트러블슈팅
|
|
317
|
-
- 이벤트 미기록
|
|
318
|
-
- Supabase URL/Key 재확인, 테이블/권한 확인
|
|
319
|
-
- 최종 아티팩트가 최종으로 저장되지 않음
|
|
320
|
-
- 익스큐터에서 `lastChunk=True` 또는 `final=True`로 보냈는지 확인
|
|
321
|
-
- payload가 래퍼와 같이 저장됨
|
|
322
|
-
- 메시지에 `parts[0].text` 또는 `parts[0].root.text`에 JSON 문자열이 들어있는지 확인
|
|
323
|
-
- 휴먼인더루프 이벤트 타입 미지정
|
|
324
|
-
- `input_required` 상태면 자동 `human_asked`로 저장됨
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
## 레퍼런스
|
|
328
|
-
- 이벤트 유틸: `new_agent_text_message`, `new_text_artifact`
|
|
329
|
-
- 서버 진입점: `ProcessGPTAgentServer.run()`
|
|
330
|
-
- 컨텍스트 확장: `ProcessGPTRequestContext.prepare_context()`
|
|
331
|
-
- 이벤트 저장: `ProcessGPTEventQueue.enqueue_event(event)` → `database.record_event`/`save_task_result`
|
|
332
|
-
- 휴먼 응답 조회: `database.fetch_human_response_sync(job_id)`
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
## 라이선스
|
|
336
|
-
해당 저장소의 라이선스 정책을 따릅니다.
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
processgpt_agent_sdk/processgpt_agent_framework.py,sha256=rTX9K-S6G_ZyzGUfIQClmJpoARqT4I51HicNw3i4y4g,16893
|
|
2
|
-
process_gpt_agent_sdk-0.3.10.dist-info/METADATA,sha256=DtoEEh--giyvPQ2tHbD3tqs_9bzbducSHSGHTj9-bSE,12201
|
|
3
|
-
process_gpt_agent_sdk-0.3.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
process_gpt_agent_sdk-0.3.10.dist-info/top_level.txt,sha256=Xe6zrj3_3Vv7d0pl5RRtenVUckwOVBVLQn2P03j5REo,21
|
|
5
|
-
process_gpt_agent_sdk-0.3.10.dist-info/RECORD,,
|
|
File without changes
|
{process_gpt_agent_sdk-0.3.10.dist-info → process_gpt_agent_sdk-0.3.12.dist-info}/top_level.txt
RENAMED
|
File without changes
|