process-gpt-agent-sdk 0.2.11__py3-none-any.whl → 0.3.4__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.

@@ -1,205 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import json
5
- import uuid
6
- from datetime import datetime, timezone
7
- from typing import Any, Optional, Dict, List
8
-
9
- from crewai.utilities.events import CrewAIEventsBus, ToolUsageStartedEvent, ToolUsageFinishedEvent
10
- from crewai.utilities.events.task_events import TaskStartedEvent, TaskCompletedEvent
11
-
12
- from .logger import handle_application_error, write_log_message
13
- from .context_manager import todo_id_var, proc_id_var, crew_type_var, form_id_var, form_key_var
14
- from ..core.database import initialize_db, get_db_client
15
-
16
-
17
- class CrewAIEventLogger:
18
- """CrewAI 이벤트 로거 - Supabase 전용"""
19
-
20
- # =============================================================================
21
- # Initialization
22
- # =============================================================================
23
- def __init__(self):
24
- """Supabase 클라이언트를 초기화한다."""
25
- initialize_db()
26
- self.supabase = get_db_client()
27
- write_log_message("CrewAIEventLogger 초기화 완료")
28
-
29
- # =============================================================================
30
- # Job ID Generation
31
- # =============================================================================
32
- def _generate_job_id(self, event_obj: Any, source: Any = None) -> str:
33
- """이벤트 객체에서 Job ID 생성"""
34
- try:
35
- if hasattr(event_obj, "task") and hasattr(event_obj.task, "id"):
36
- return str(event_obj.task.id)
37
- if source and hasattr(source, "task") and hasattr(source.task, "id"):
38
- return str(source.task.id)
39
- except Exception:
40
- pass
41
- return "unknown"
42
-
43
- # =============================================================================
44
- # Record Creation
45
- # =============================================================================
46
- def _create_event_record(
47
- self,
48
- event_type: str,
49
- data: Dict[str, Any],
50
- job_id: str,
51
- crew_type: str,
52
- todo_id: Optional[str],
53
- proc_inst_id: Optional[str],
54
- ) -> Dict[str, Any]:
55
- """이벤트 레코드 생성"""
56
- return {
57
- "id": str(uuid.uuid4()),
58
- "job_id": job_id,
59
- "todo_id": todo_id,
60
- "proc_inst_id": proc_inst_id,
61
- "event_type": event_type,
62
- "crew_type": crew_type,
63
- "data": data,
64
- "timestamp": datetime.now(timezone.utc).isoformat(),
65
- }
66
-
67
- # =============================================================================
68
- # Parsing Helpers
69
- # =============================================================================
70
- def _parse_json_text(self, text: str) -> Any:
71
- """JSON 문자열을 객체로 파싱하거나 원본 반환"""
72
- try:
73
- return json.loads(text)
74
- except:
75
- return text
76
-
77
- def _parse_output(self, output: Any) -> Any:
78
- """output 또는 raw 텍스트를 파싱해 반환"""
79
- if not output:
80
- return ""
81
- text = getattr(output, "raw", None) or (output if isinstance(output, str) else "")
82
- return self._parse_json_text(text)
83
-
84
- def _parse_tool_args(self, args_text: str) -> Optional[str]:
85
- """tool_args에서 query 키 추출"""
86
- try:
87
- args = json.loads(args_text or "{}")
88
- return args.get("query")
89
- except Exception:
90
- return None
91
-
92
- # =============================================================================
93
- # Formatting Helpers
94
- # =============================================================================
95
- def _format_plans_md(self, plans: List[Dict[str, Any]]) -> str:
96
- """list_of_plans_per_task 형식을 Markdown 문자열로 변환"""
97
- lines: List[str] = []
98
- for idx, item in enumerate(plans or [], 1):
99
- task = item.get("task", "")
100
- plan = item.get("plan", "")
101
- lines.append(f"## {idx}. {task}")
102
- lines.append("")
103
- if isinstance(plan, list):
104
- for line in plan:
105
- lines.append(str(line))
106
- elif isinstance(plan, str):
107
- for line in plan.split("\n"):
108
- lines.append(line)
109
- else:
110
- lines.append(str(plan))
111
- lines.append("")
112
- return "\n".join(lines).strip()
113
-
114
- # =============================================================================
115
- # Data Extraction
116
- # =============================================================================
117
- def _extract_event_data(self, event_obj: Any, source: Any = None) -> Dict[str, Any]:
118
- """이벤트 타입별 데이터 추출"""
119
- etype = getattr(event_obj, "type", None) or type(event_obj).__name__
120
- if etype == "task_started":
121
- agent = getattr(getattr(event_obj, "task", None), "agent", None)
122
- return {
123
- "role": getattr(agent, "role", "Unknown"),
124
- "goal": getattr(agent, "goal", "Unknown"),
125
- "agent_profile": getattr(agent, "profile", None) or "/images/chat-icon.png",
126
- "name": getattr(agent, "name", "Unknown"),
127
- }
128
- if etype == "task_completed":
129
- result = self._parse_output(getattr(event_obj, "output", None))
130
- if isinstance(result, dict) and "list_of_plans_per_task" in result:
131
- md = self._format_plans_md(result.get("list_of_plans_per_task") or [])
132
- return {"plans": md}
133
- return {"result": result}
134
-
135
- if etype in ("tool_usage_started", "tool_usage_finished") or str(etype).startswith("tool_"):
136
- return {
137
- "tool_name": getattr(event_obj, "tool_name", None),
138
- "query": self._parse_tool_args(getattr(event_obj, "tool_args", "")),
139
- }
140
- return {"info": f"Event type: {etype}"}
141
-
142
- # =============================================================================
143
- # Event Saving
144
- # =============================================================================
145
- def _save_event(self, record: Dict[str, Any]) -> None:
146
- """Supabase에 이벤트 레코드 저장 (간단 재시도 포함)"""
147
- payload = json.loads(json.dumps(record, default=str))
148
- for attempt in range(1, 4):
149
- try:
150
- self.supabase.table("events").insert(payload).execute()
151
- return
152
- except Exception as e:
153
- if attempt < 3:
154
- handle_application_error("이벤트저장오류(재시도)", e, raise_error=False)
155
- import time
156
- time.sleep(0.3 * attempt)
157
- continue
158
- handle_application_error("이벤트저장오류(최종)", e, raise_error=False)
159
- return
160
-
161
- # =============================================================================
162
- # Event Handling
163
- # =============================================================================
164
- def on_event(self, event_obj: Any, source: Any = None) -> None:
165
- """이벤트 수신부터 DB 저장까지 처리"""
166
- etype = getattr(event_obj, "type", None) or type(event_obj).__name__
167
- ALLOWED = {"task_started", "task_completed", "tool_usage_started", "tool_usage_finished"}
168
- if etype not in ALLOWED:
169
- return
170
- try:
171
- job_id = self._generate_job_id(event_obj, source)
172
- data = self._extract_event_data(event_obj, source)
173
- crew_type = crew_type_var.get() or "action"
174
- rec = self._create_event_record(etype, data, job_id, crew_type, todo_id_var.get(), proc_id_var.get())
175
- self._save_event(rec)
176
- write_log_message(f"[{etype}] [{job_id[:8]}] 저장 완료")
177
- except Exception as e:
178
- handle_application_error("이벤트처리오류", e, raise_error=False)
179
-
180
-
181
- # =============================================================================
182
- # CrewConfigManager
183
- # 설명: 이벤트 리스너를 프로세스 단위로 1회 등록
184
- # =============================================================================
185
- class CrewConfigManager:
186
- """글로벌 CrewAI 이벤트 리스너 등록 매니저"""
187
- _registered_by_pid: set[int] = set()
188
-
189
- def __init__(self) -> None:
190
- self.logger = CrewAIEventLogger()
191
- self._register_once_per_process()
192
-
193
- def _register_once_per_process(self) -> None:
194
- """현재 프로세스에만 한 번 이벤트 리스너를 등록한다."""
195
- try:
196
- pid = os.getpid()
197
- if pid in self._registered_by_pid:
198
- return
199
- bus = CrewAIEventsBus()
200
- for evt in (TaskStartedEvent, TaskCompletedEvent, ToolUsageStartedEvent, ToolUsageFinishedEvent):
201
- bus.on(evt)(lambda source, event, logger=self.logger: logger.on_event(event, source))
202
- self._registered_by_pid.add(pid)
203
- write_log_message("CrewAI event listeners 등록 완료")
204
- except Exception as e:
205
- handle_application_error("CrewAI 이벤트 버스 등록 실패", e, raise_error=False)
@@ -1,72 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Dict
4
- import uuid
5
-
6
- from a2a.server.events import Event
7
- from .logger import handle_application_error, write_log_message
8
- from ..core.database import record_event, save_task_result
9
- from ..tools.safe_tool_loader import SafeToolLoader
10
-
11
-
12
- # =============================================================================
13
- # 이벤트 변환: Event 또는 dict를 표준 dict로 통일
14
- # =============================================================================
15
-
16
- def convert_event_to_dictionary(event: Event) -> Dict[str, Any]:
17
- """Event/dict를 표준 dict로 변환한다."""
18
- try:
19
- # 이미 dict로 전달된 경우 그대로 사용
20
- if isinstance(event, dict):
21
- return event
22
- # Event 객체면 공개 필드만 추출
23
- if hasattr(event, "__dict__"):
24
- return {k: v for k, v in event.__dict__.items() if not k.startswith("_")}
25
- # 알 수 없는 타입은 문자열로 보존
26
- return {"type": "event", "data": str(event)}
27
- except Exception as e:
28
- handle_application_error("event dict 변환 실패", e, raise_error=False)
29
- return {"type": "event", "data": str(event)}
30
-
31
-
32
- # =============================================================================
33
- # 이벤트 처리: type에 따라 저장 위치 분기
34
- # =============================================================================
35
-
36
- async def process_event_message(todo: Dict[str, Any], event: Event) -> None:
37
- """이벤트 타입별로 todolist/events에 저장하거나 리소스 정리."""
38
- try:
39
- data = convert_event_to_dictionary(event)
40
- evt_type = str(data.get("type") or data.get("event_type") or "").lower()
41
-
42
- # done: 종료 이벤트 → 기록 후 MCP 정리
43
- if evt_type == "done":
44
- payload = data.get("data") or {}
45
- if isinstance(payload, dict) and "id" not in payload:
46
- payload["id"] = str(uuid.uuid4())
47
- await record_event(payload)
48
- try:
49
- SafeToolLoader.shutdown_all_adapters()
50
- write_log_message("MCP 리소스 정리 완료")
51
- except Exception as ce:
52
- handle_application_error("MCP 리소스 정리 실패", ce, raise_error=False)
53
- return
54
-
55
- # output: 결과 저장만 수행
56
- if evt_type == "output":
57
- payload = data.get("data") or {}
58
- is_final = bool(payload.get("final") or payload.get("is_final")) if isinstance(payload, dict) else False
59
- content = payload.get("content") or payload.get("data") if isinstance(payload, dict) else payload
60
- await save_task_result(str(todo.get("id")), content, final=is_final)
61
- return
62
-
63
- # event : 일반 이벤트 저장 (워커 데이터 그대로 보존)
64
- if evt_type == "event":
65
- payload = data.get("data") or {}
66
- if isinstance(payload, dict) and "id" not in payload:
67
- payload["id"] = str(uuid.uuid4())
68
- await record_event(payload)
69
- return
70
-
71
- except Exception as e:
72
- handle_application_error("process_event_message 처리 실패", e, raise_error=False)
@@ -1,97 +0,0 @@
1
- import logging
2
- import os
3
- import traceback
4
- from typing import Optional, Dict
5
-
6
-
7
- # Configure root logger only once (idempotent)
8
- if not logging.getLogger().handlers:
9
- # LOG_LEVEL 환경변수 읽기
10
- log_level = os.getenv("LOG_LEVEL", "INFO").upper()
11
- if log_level == "DEBUG":
12
- level = logging.DEBUG
13
- elif log_level == "INFO":
14
- level = logging.INFO
15
- elif log_level == "WARNING":
16
- level = logging.WARNING
17
- elif log_level == "ERROR":
18
- level = logging.ERROR
19
- else:
20
- level = logging.INFO
21
-
22
- logging.basicConfig(
23
- level=level,
24
- format="%(asctime)s %(levelname)s %(name)s - %(message)s",
25
- )
26
-
27
- LOGGER_NAME = os.getenv("LOGGER_NAME") or "processgpt"
28
- APPLICATION_LOGGER = logging.getLogger(LOGGER_NAME)
29
-
30
- # Application logger도 같은 레벨로 설정
31
- log_level = os.getenv("LOG_LEVEL", "INFO").upper()
32
- if log_level == "DEBUG":
33
- APPLICATION_LOGGER.setLevel(logging.DEBUG)
34
- elif log_level == "INFO":
35
- APPLICATION_LOGGER.setLevel(logging.INFO)
36
- elif log_level == "WARNING":
37
- APPLICATION_LOGGER.setLevel(logging.WARNING)
38
- elif log_level == "ERROR":
39
- APPLICATION_LOGGER.setLevel(logging.ERROR)
40
-
41
- # 디버그 레벨 상수 정의
42
- DEBUG_LEVEL_NONE = 0 # 디버그 로그 없음
43
- DEBUG_LEVEL_BASIC = 1 # 기본 디버그 로그 (INFO 레벨)
44
- DEBUG_LEVEL_DETAILED = 2 # 상세 디버그 로그 (DEBUG 레벨)
45
- DEBUG_LEVEL_VERBOSE = 3 # 매우 상세한 디버그 로그 (DEBUG 레벨 + 추가 정보)
46
-
47
- # 환경변수에서 디버그 레벨 읽기
48
- DEBUG_LEVEL = int(os.getenv("DEBUG_LEVEL", "1"))
49
-
50
-
51
- def set_application_logger_name(name: str) -> None:
52
- """애플리케이션 로거 이름을 런타임에 변경한다."""
53
- global APPLICATION_LOGGER
54
- APPLICATION_LOGGER = logging.getLogger(name or "processgpt")
55
-
56
-
57
- def write_log_message(message: str, level: int = logging.INFO, debug_level: int = DEBUG_LEVEL_BASIC) -> None:
58
- """로그 메시지를 쓴다. 디버그 레벨에 따라 출력 여부를 결정한다."""
59
- # 디버그 레벨 체크
60
- if debug_level > DEBUG_LEVEL:
61
- return
62
-
63
- # DEBUG 레벨 로그의 경우 로거 레벨도 확인
64
- if level == logging.DEBUG and DEBUG_LEVEL < DEBUG_LEVEL_DETAILED:
65
- return
66
-
67
- spaced = os.getenv("LOG_SPACED", "1") != "0"
68
- suffix = "\n" if spaced else ""
69
- APPLICATION_LOGGER.log(level, f"{message}{suffix}")
70
-
71
-
72
- def write_debug_message(message: str, debug_level: int = DEBUG_LEVEL_BASIC) -> None:
73
- """디버그 전용 로그 메시지를 쓴다."""
74
- write_log_message(message, logging.DEBUG, debug_level)
75
-
76
-
77
- def write_info_message(message: str, debug_level: int = DEBUG_LEVEL_BASIC) -> None:
78
- """정보 로그 메시지를 쓴다."""
79
- write_log_message(message, logging.INFO, debug_level)
80
-
81
-
82
- def set_debug_level(level: int) -> None:
83
- """런타임에 디버그 레벨을 설정한다."""
84
- global DEBUG_LEVEL
85
- DEBUG_LEVEL = level
86
- write_info_message(f"디버그 레벨이 {level}로 설정되었습니다.", DEBUG_LEVEL_BASIC)
87
-
88
-
89
- def handle_application_error(title: str, error: Exception, *, raise_error: bool = True, extra: Optional[Dict] = None) -> None:
90
- """예외 상황을 처리한다."""
91
- spaced = os.getenv("LOG_SPACED", "1") != "0"
92
- suffix = "\n" if spaced else ""
93
- context = f" | extra={extra}" if extra else ""
94
- APPLICATION_LOGGER.error(f"{title}: {error}{context}{suffix}")
95
- APPLICATION_LOGGER.error(traceback.format_exc())
96
- if raise_error:
97
- raise error
@@ -1,146 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- import os
5
- import json
6
- import asyncio
7
- from typing import Any, Tuple
8
-
9
- import openai
10
- from .logger import handle_application_error, write_log_message
11
-
12
- # =============================================================================
13
- # 요약기(Summarizer)
14
- # 설명: 출력/피드백/현재 내용으로부터 OpenAI를 사용해 간단 요약을 생성한다.
15
- # =============================================================================
16
-
17
- async def summarize_async(outputs: Any, feedbacks: Any, contents: Any = None) -> Tuple[str, str]:
18
- """(output_summary, feedback_summary)를 비동기로 생성해 반환한다.
19
- 키 없음/오류 시 빈 문자열 폴백, 취소는 상위로 전파."""
20
- outputs_str = _convert_to_string(outputs).strip()
21
- feedbacks_str = _convert_to_string(feedbacks).strip()
22
- contents_str = _convert_to_string(contents).strip()
23
-
24
- output_summary = ""
25
- feedback_summary = ""
26
-
27
- if outputs_str and outputs_str not in ("[]", "{}", "[{}]"):
28
- write_log_message("요약 호출(이전결과물)")
29
- output_prompt = _create_output_summary_prompt(outputs_str)
30
- output_summary = await _call_openai_api_async(output_prompt, task_name="output")
31
-
32
- if feedbacks_str and feedbacks_str not in ("[]", "{}"):
33
- write_log_message("요약 호출(피드백)")
34
- feedback_prompt = _create_feedback_summary_prompt(feedbacks_str, contents_str)
35
- feedback_summary = await _call_openai_api_async(feedback_prompt, task_name="feedback")
36
-
37
- return output_summary or "", feedback_summary or ""
38
-
39
-
40
- # =============================================================================
41
- # 헬퍼: 문자열 변환
42
- # =============================================================================
43
-
44
- def _convert_to_string(data: Any) -> str:
45
- """임의 데이터를 안전하게 문자열로 변환한다."""
46
- if data is None:
47
- return ""
48
- if isinstance(data, str):
49
- return data
50
- try:
51
- return json.dumps(data, ensure_ascii=False)
52
- except Exception:
53
- return str(data)
54
-
55
-
56
- # =============================================================================
57
- # 헬퍼: 프롬프트 생성
58
- # =============================================================================
59
-
60
- def _create_output_summary_prompt(outputs_str: str) -> str:
61
- """결과물 요약용 사용자 프롬프트를 생성한다."""
62
- return (
63
- "다음 작업 결과를 정리해주세요:\n\n"
64
- f"{outputs_str}\n\n"
65
- "처리 방식:\n"
66
- "- 짧은 내용은 요약하지 말고 그대로 유지 (정보 손실 방지)\n"
67
- "- 긴 내용만 적절히 요약하여 핵심 정보 전달\n"
68
- "- 수치, 목차, 인물명, 물건명, 날짜, 시간 등 객관적 정보는 반드시 포함\n"
69
- "- 왜곡이나 의미 변경 금지, 원본 의미 보존\n"
70
- "- 중복만 정리하고 핵심 내용은 모두 보존\n"
71
- "- 하나의 통합된 문맥으로 작성"
72
- )
73
-
74
-
75
- def _create_feedback_summary_prompt(feedbacks_str: str, contents_str: str = "") -> str:
76
- """피드백/현재 결과물을 통합 요약하는 사용자 프롬프트를 생성한다."""
77
- feedback_section = (
78
- f"=== 피드백 내용 ===\n{feedbacks_str}" if feedbacks_str and feedbacks_str.strip() else ""
79
- )
80
- content_section = (
81
- f"=== 현재 결과물/작업 내용 ===\n{contents_str}" if contents_str and contents_str.strip() else ""
82
- )
83
- return (
84
- "다음은 사용자의 피드백과 결과물입니다. 이를 분석하여 통합된 피드백을 작성해주세요:\n\n"
85
- f"{feedback_section}\n\n{content_section}\n\n"
86
- "상황 분석 및 처리 방식:\n"
87
- "- 현재 결과물을 보고 문제/개선 필요점 판단\n"
88
- "- 최신 피드백을 최우선으로 반영\n"
89
- "- 실행 가능한 개선사항 제시\n"
90
- "- 하나의 통합된 문장으로 작성 (최대 2500자)"
91
- )
92
-
93
-
94
- # =============================================================================
95
- # 헬퍼: 시스템 프롬프트 선택
96
- # =============================================================================
97
-
98
- def _get_system_prompt(task_name: str) -> str:
99
- """작업 종류에 맞는 시스템 프롬프트를 반환한다."""
100
- if task_name == "feedback":
101
- return (
102
- "당신은 피드백 정리 전문가입니다. 최신 피드백을 우선 반영하고,"
103
- " 문맥을 연결하여 하나의 완전한 요청으로 통합해 주세요."
104
- )
105
- return (
106
- "당신은 결과물 요약 전문가입니다. 긴 내용만 요약하고,"
107
- " 수치/고유명/날짜 등 객관 정보를 보존해 주세요."
108
- )
109
-
110
-
111
- # =============================================================================
112
- # 외부 호출: OpenAI API
113
- # =============================================================================
114
-
115
- async def _call_openai_api_async(prompt: str, task_name: str) -> str:
116
- """OpenAI 비동기 API를 호출해 요약 텍스트를 생성한다."""
117
-
118
- if not (os.getenv("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY_BETA")):
119
- write_log_message("요약 비활성화: OPENAI_API_KEY 미설정")
120
- return ""
121
-
122
- client = openai.AsyncOpenAI()
123
- system_prompt = _get_system_prompt(task_name)
124
- model = os.getenv("OPENAI_SUMMARY_MODEL", "gpt-4o-mini")
125
-
126
- for attempt in range(1, 4):
127
- try:
128
- resp = await client.chat.completions.create(
129
- model=model,
130
- messages=[
131
- {"role": "system", "content": system_prompt},
132
- {"role": "user", "content": prompt},
133
- ],
134
- temperature=0.1,
135
- timeout=30.0,
136
- )
137
- return (resp.choices[0].message.content or "").strip()
138
- except asyncio.CancelledError:
139
- raise
140
- except Exception as e:
141
- if attempt < 3:
142
- handle_application_error("요약 호출 오류(재시도)", e, raise_error=False, extra={"attempt": attempt})
143
- await asyncio.sleep(0.8 * (2 ** (attempt - 1)))
144
- continue
145
- handle_application_error("요약 호출 최종 실패", e, raise_error=False)
146
- return ""