process-gpt-agent-sdk 0.1.5__py3-none-any.whl → 0.1.7__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.1.5.dist-info → process_gpt_agent_sdk-0.1.7.dist-info}/METADATA +1 -1
- process_gpt_agent_sdk-0.1.7.dist-info/RECORD +18 -0
- processgpt_agent_sdk/core/database.py +137 -2
- processgpt_agent_sdk/server.py +68 -61
- processgpt_agent_sdk/tools/human_query_tool.py +203 -0
- processgpt_agent_sdk/tools/knowledge_tools.py +206 -206
- processgpt_agent_sdk/tools/safe_tool_loader.py +41 -47
- processgpt_agent_sdk/utils/context_manager.py +5 -1
- processgpt_agent_sdk/utils/crewai_event_listener.py +8 -8
- processgpt_agent_sdk/utils/event_handler.py +30 -12
- processgpt_agent_sdk/utils/logger.py +6 -6
- process_gpt_agent_sdk-0.1.5.dist-info/RECORD +0 -17
- {process_gpt_agent_sdk-0.1.5.dist-info → process_gpt_agent_sdk-0.1.7.dist-info}/WHEEL +0 -0
- {process_gpt_agent_sdk-0.1.5.dist-info → process_gpt_agent_sdk-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Optional, List, Type
|
|
3
|
-
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
|
4
|
-
from crewai.tools import BaseTool
|
|
5
|
-
from dotenv import load_dotenv
|
|
6
|
-
from mem0 import Memory
|
|
7
|
-
import requests
|
|
8
|
-
from ..utils.logger import
|
|
9
|
-
|
|
10
|
-
# ============================================================================
|
|
11
|
-
# 설정 및 초기화
|
|
12
|
-
# ============================================================================
|
|
13
|
-
|
|
14
|
-
load_dotenv()
|
|
15
|
-
|
|
16
|
-
DB_USER = os.getenv("DB_USER")
|
|
17
|
-
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
|
18
|
-
DB_HOST = os.getenv("DB_HOST")
|
|
19
|
-
DB_PORT = os.getenv("DB_PORT")
|
|
20
|
-
DB_NAME = os.getenv("DB_NAME")
|
|
21
|
-
|
|
22
|
-
CONNECTION_STRING = None
|
|
23
|
-
if all([DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME]):
|
|
24
|
-
CONNECTION_STRING = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
|
25
|
-
else:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# ============================================================================
|
|
29
|
-
# 스키마 정의
|
|
30
|
-
# ============================================================================
|
|
31
|
-
|
|
32
|
-
class KnowledgeQuerySchema(BaseModel):
|
|
33
|
-
query: str = Field(..., description="검색할 지식 쿼리")
|
|
34
|
-
|
|
35
|
-
@field_validator('query', mode='before')
|
|
36
|
-
@classmethod
|
|
37
|
-
def validate_query(cls, v):
|
|
38
|
-
import json
|
|
39
|
-
if isinstance(v, dict):
|
|
40
|
-
for k in ("description", "query", "q", "text", "message"):
|
|
41
|
-
if k in v and v[k]:
|
|
42
|
-
return str(v[k])
|
|
43
|
-
return "" if not v else json.dumps(v, ensure_ascii=False)
|
|
44
|
-
return v if isinstance(v, str) else str(v)
|
|
45
|
-
|
|
46
|
-
# ============================================================================
|
|
47
|
-
# 지식 검색 도구
|
|
48
|
-
# ============================================================================
|
|
49
|
-
|
|
50
|
-
class Mem0Tool(BaseTool):
|
|
51
|
-
"""Supabase 기반 mem0 지식 검색 도구 - 에이전트별"""
|
|
52
|
-
name: str = "mem0"
|
|
53
|
-
description: str = (
|
|
54
|
-
"🧠 에이전트별 개인 지식 저장소 검색 도구\n\n"
|
|
55
|
-
"🚨 필수 검색 순서: 작업 전 반드시 피드백부터 검색!\n\n"
|
|
56
|
-
"저장된 정보:\n"
|
|
57
|
-
"🔴 과거 동일한 작업에 대한 피드백 및 교훈 (최우선 검색 대상)\n"
|
|
58
|
-
"🔴 과거 실패 사례 및 개선 방안\n"
|
|
59
|
-
"• 객관적 정보 (사람명, 수치, 날짜, 사물 등)\n"
|
|
60
|
-
"검색 목적:\n"
|
|
61
|
-
"- 작업지시사항을 올바르게 수행하기 위해 필요한 정보(매개변수, 제약, 의존성)와\n"
|
|
62
|
-
" 안전 수행을 위한 피드백/주의사항을 찾기 위함\n"
|
|
63
|
-
"- 과거 실패 경험을 통한 실수 방지\n"
|
|
64
|
-
"- 정확한 객관적 정보 조회\n\n"
|
|
65
|
-
"사용 지침:\n"
|
|
66
|
-
"- 현재 작업 맥락(사용자 요청, 시스템/도구 출력, 최근 단계)을 근거로 자연어의 완전한 문장으로 질의하세요.\n"
|
|
67
|
-
"- 핵심 키워드 + 엔터티(고객명, 테이블명, 날짜 등) + 제약(환경/범위)을 조합하세요.\n"
|
|
68
|
-
"- 동의어/영문 용어를 섞어 2~3개의 표현으로 재질의하여 누락을 줄이세요.\n"
|
|
69
|
-
"- 필요한 경우 좁은 쿼리 → 넓은 쿼리 순서로 반복 검색하세요. (필요 시 기간/버전 범위 명시)\n"
|
|
70
|
-
"- 동일 정보를 다른 표현으로 재질의하며, 최신/가장 관련 결과를 우선 검토하세요.\n\n"
|
|
71
|
-
"⚡ 핵심: 어떤 작업이든 시작 전에, 해당 작업을 안전하게 수행하기 위한 피드백/주의사항과\n"
|
|
72
|
-
" 필수 매개변수를 먼저 질의하여 확보하세요!"
|
|
73
|
-
)
|
|
74
|
-
args_schema: Type[KnowledgeQuerySchema] = KnowledgeQuerySchema
|
|
75
|
-
_tenant_id: Optional[str] = PrivateAttr()
|
|
76
|
-
_user_id: Optional[str] = PrivateAttr()
|
|
77
|
-
_namespace: Optional[str] = PrivateAttr()
|
|
78
|
-
_memory: Optional[Memory] = PrivateAttr(default=None)
|
|
79
|
-
|
|
80
|
-
def __init__(self, tenant_id: str = None, user_id: str = None, **kwargs):
|
|
81
|
-
super().__init__(**kwargs)
|
|
82
|
-
self._tenant_id = tenant_id
|
|
83
|
-
self._user_id = user_id
|
|
84
|
-
self._namespace = user_id
|
|
85
|
-
self._memory = self._initialize_memory()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _initialize_memory(self) -> Optional[Memory]:
|
|
89
|
-
"""Memory 인스턴스 초기화 - 에이전트별"""
|
|
90
|
-
if not CONNECTION_STRING:
|
|
91
|
-
return None
|
|
92
|
-
config = {
|
|
93
|
-
"vector_store": {
|
|
94
|
-
"provider": "supabase",
|
|
95
|
-
"config": {
|
|
96
|
-
"connection_string": CONNECTION_STRING,
|
|
97
|
-
"collection_name": "memories",
|
|
98
|
-
"index_method": "hnsw",
|
|
99
|
-
"index_measure": "cosine_distance"
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return Memory.from_config(config_dict=config)
|
|
104
|
-
|
|
105
|
-
def _run(self, query: str) -> str:
|
|
106
|
-
"""지식 검색 및 결과 반환 - 에이전트별 메모리에서"""
|
|
107
|
-
if not query:
|
|
108
|
-
return "검색할 쿼리를 입력해주세요."
|
|
109
|
-
if not self._user_id:
|
|
110
|
-
return "개인지식 검색 비활성화: user_id 없음"
|
|
111
|
-
if not self._memory:
|
|
112
|
-
return "mem0 비활성화: DB 연결 정보(DB_*)가 설정되지 않았습니다."
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
|
|
116
|
-
results = self._memory.search(query, user_id=self._user_id)
|
|
117
|
-
hits = results.get("results", [])
|
|
118
|
-
|
|
119
|
-
THRESHOLD = 0.5
|
|
120
|
-
MIN_RESULTS = 5
|
|
121
|
-
hits_sorted = sorted(hits, key=lambda x: x.get("score", 0), reverse=True)
|
|
122
|
-
filtered_hits = [h for h in hits_sorted if h.get("score", 0) >= THRESHOLD]
|
|
123
|
-
if len(filtered_hits) < MIN_RESULTS:
|
|
124
|
-
filtered_hits = hits_sorted[:MIN_RESULTS]
|
|
125
|
-
hits = filtered_hits
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if not hits:
|
|
130
|
-
return f"'{query}'에 대한 개인 지식이 없습니다."
|
|
131
|
-
|
|
132
|
-
return self._format_results(hits)
|
|
133
|
-
|
|
134
|
-
except Exception as e:
|
|
135
|
-
|
|
136
|
-
return f"지식검색오류: {e}"
|
|
137
|
-
|
|
138
|
-
def _format_results(self, hits: List[dict]) -> str:
|
|
139
|
-
"""검색 결과 포맷팅"""
|
|
140
|
-
items = []
|
|
141
|
-
for idx, hit in enumerate(hits, start=1):
|
|
142
|
-
memory_text = hit.get("memory", "")
|
|
143
|
-
score = hit.get("score", 0)
|
|
144
|
-
items.append(f"개인지식 {idx} (관련도: {score:.2f})\n{memory_text}")
|
|
145
|
-
|
|
146
|
-
return "\n\n".join(items)
|
|
147
|
-
|
|
148
|
-
# ============================================================================
|
|
149
|
-
# 사내 문서 검색 (memento) 도구
|
|
150
|
-
# ============================================================================
|
|
151
|
-
|
|
152
|
-
class MementoQuerySchema(BaseModel):
|
|
153
|
-
query: str = Field(..., description="검색 키워드 또는 질문")
|
|
154
|
-
|
|
155
|
-
class MementoTool(BaseTool):
|
|
156
|
-
"""사내 문서 검색을 수행하는 도구"""
|
|
157
|
-
name: str = "memento"
|
|
158
|
-
description: str = (
|
|
159
|
-
"🔒 보안 민감한 사내 문서 검색 도구\n\n"
|
|
160
|
-
"저장된 정보:\n"
|
|
161
|
-
"• 보안 민감한 사내 기밀 문서\n"
|
|
162
|
-
"• 대용량 사내 문서 및 정책 자료\n"
|
|
163
|
-
"• 객관적이고 정확한 회사 내부 지식\n"
|
|
164
|
-
"• 업무 프로세스, 규정, 기술 문서\n\n"
|
|
165
|
-
"검색 목적:\n"
|
|
166
|
-
"- 작업지시사항을 올바르게 수행하기 위한 회사 정책/규정/프로세스/매뉴얼 확보\n"
|
|
167
|
-
"- 최신 버전의 표준과 가이드라인 확인\n\n"
|
|
168
|
-
"사용 지침:\n"
|
|
169
|
-
"- 현재 작업/요청과 직접 연결된 문맥을 담아 자연어의 완전한 문장으로 질의하세요.\n"
|
|
170
|
-
"- 문서 제목/버전/담당조직/기간/환경(프로덕션·스테이징·모듈 등) 조건을 명확히 포함하세요.\n"
|
|
171
|
-
"- 약어·정식명칭, 한·영 용어를 함께 사용해 2~3회 재질의하며 누락을 줄이세요.\n"
|
|
172
|
-
"- 처음엔 좁게, 필요 시 점진적으로 범위를 넓혀 검색하세요.\n\n"
|
|
173
|
-
"⚠️ 보안 민감 정보 포함 - 적절한 권한과 용도로만 사용"
|
|
174
|
-
)
|
|
175
|
-
args_schema: Type[MementoQuerySchema] = MementoQuerySchema
|
|
176
|
-
_tenant_id: str = PrivateAttr()
|
|
177
|
-
|
|
178
|
-
def __init__(self, tenant_id: str = "localhost", **kwargs):
|
|
179
|
-
super().__init__(**kwargs)
|
|
180
|
-
self._tenant_id = tenant_id
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def _run(self, query: str) -> str:
|
|
184
|
-
try:
|
|
185
|
-
|
|
186
|
-
response = requests.post(
|
|
187
|
-
"http://memento.process-gpt.io/retrieve",
|
|
188
|
-
json={"query": query, "options": {"tenant_id": self._tenant_id}}
|
|
189
|
-
)
|
|
190
|
-
if response.status_code != 200:
|
|
191
|
-
return f"API 오류: {response.status_code}"
|
|
192
|
-
data = response.json()
|
|
193
|
-
if not data.get("response"):
|
|
194
|
-
return f"테넌트 '{self._tenant_id}'에서 '{query}' 검색 결과가 없습니다."
|
|
195
|
-
results = []
|
|
196
|
-
docs = data.get("response", [])
|
|
197
|
-
|
|
198
|
-
for doc in docs:
|
|
199
|
-
fname = doc.get('metadata', {}).get('file_name', 'unknown')
|
|
200
|
-
idx = doc.get('metadata', {}).get('chunk_index', 'unknown')
|
|
201
|
-
content = doc.get('page_content', '')
|
|
202
|
-
results.append(f"📄 파일: {fname} (청크 #{idx})\n내용: {content}\n---")
|
|
203
|
-
return f"테넌트 '{self._tenant_id}'에서 '{query}' 검색 결과:\n\n" + "\n".join(results)
|
|
204
|
-
except Exception as e:
|
|
205
|
-
|
|
206
|
-
return f"문서검색오류: {e}"
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, List, Type
|
|
3
|
+
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
|
4
|
+
from crewai.tools import BaseTool
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
from mem0 import Memory
|
|
7
|
+
import requests
|
|
8
|
+
from ..utils.logger import write_log_message, handle_application_error
|
|
9
|
+
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# 설정 및 초기화
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
DB_USER = os.getenv("DB_USER")
|
|
17
|
+
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
|
18
|
+
DB_HOST = os.getenv("DB_HOST")
|
|
19
|
+
DB_PORT = os.getenv("DB_PORT")
|
|
20
|
+
DB_NAME = os.getenv("DB_NAME")
|
|
21
|
+
|
|
22
|
+
CONNECTION_STRING = None
|
|
23
|
+
if all([DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME]):
|
|
24
|
+
CONNECTION_STRING = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
|
25
|
+
else:
|
|
26
|
+
write_log_message("mem0 연결 문자열이 설정되지 않았습니다(DB_* env). 기능은 제한될 수 있습니다.")
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# 스키마 정의
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
class KnowledgeQuerySchema(BaseModel):
|
|
33
|
+
query: str = Field(..., description="검색할 지식 쿼리")
|
|
34
|
+
|
|
35
|
+
@field_validator('query', mode='before')
|
|
36
|
+
@classmethod
|
|
37
|
+
def validate_query(cls, v):
|
|
38
|
+
import json
|
|
39
|
+
if isinstance(v, dict):
|
|
40
|
+
for k in ("description", "query", "q", "text", "message"):
|
|
41
|
+
if k in v and v[k]:
|
|
42
|
+
return str(v[k])
|
|
43
|
+
return "" if not v else json.dumps(v, ensure_ascii=False)
|
|
44
|
+
return v if isinstance(v, str) else str(v)
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# 지식 검색 도구
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
class Mem0Tool(BaseTool):
|
|
51
|
+
"""Supabase 기반 mem0 지식 검색 도구 - 에이전트별"""
|
|
52
|
+
name: str = "mem0"
|
|
53
|
+
description: str = (
|
|
54
|
+
"🧠 에이전트별 개인 지식 저장소 검색 도구\n\n"
|
|
55
|
+
"🚨 필수 검색 순서: 작업 전 반드시 피드백부터 검색!\n\n"
|
|
56
|
+
"저장된 정보:\n"
|
|
57
|
+
"🔴 과거 동일한 작업에 대한 피드백 및 교훈 (최우선 검색 대상)\n"
|
|
58
|
+
"🔴 과거 실패 사례 및 개선 방안\n"
|
|
59
|
+
"• 객관적 정보 (사람명, 수치, 날짜, 사물 등)\n"
|
|
60
|
+
"검색 목적:\n"
|
|
61
|
+
"- 작업지시사항을 올바르게 수행하기 위해 필요한 정보(매개변수, 제약, 의존성)와\n"
|
|
62
|
+
" 안전 수행을 위한 피드백/주의사항을 찾기 위함\n"
|
|
63
|
+
"- 과거 실패 경험을 통한 실수 방지\n"
|
|
64
|
+
"- 정확한 객관적 정보 조회\n\n"
|
|
65
|
+
"사용 지침:\n"
|
|
66
|
+
"- 현재 작업 맥락(사용자 요청, 시스템/도구 출력, 최근 단계)을 근거로 자연어의 완전한 문장으로 질의하세요.\n"
|
|
67
|
+
"- 핵심 키워드 + 엔터티(고객명, 테이블명, 날짜 등) + 제약(환경/범위)을 조합하세요.\n"
|
|
68
|
+
"- 동의어/영문 용어를 섞어 2~3개의 표현으로 재질의하여 누락을 줄이세요.\n"
|
|
69
|
+
"- 필요한 경우 좁은 쿼리 → 넓은 쿼리 순서로 반복 검색하세요. (필요 시 기간/버전 범위 명시)\n"
|
|
70
|
+
"- 동일 정보를 다른 표현으로 재질의하며, 최신/가장 관련 결과를 우선 검토하세요.\n\n"
|
|
71
|
+
"⚡ 핵심: 어떤 작업이든 시작 전에, 해당 작업을 안전하게 수행하기 위한 피드백/주의사항과\n"
|
|
72
|
+
" 필수 매개변수를 먼저 질의하여 확보하세요!"
|
|
73
|
+
)
|
|
74
|
+
args_schema: Type[KnowledgeQuerySchema] = KnowledgeQuerySchema
|
|
75
|
+
_tenant_id: Optional[str] = PrivateAttr()
|
|
76
|
+
_user_id: Optional[str] = PrivateAttr()
|
|
77
|
+
_namespace: Optional[str] = PrivateAttr()
|
|
78
|
+
_memory: Optional[Memory] = PrivateAttr(default=None)
|
|
79
|
+
|
|
80
|
+
def __init__(self, tenant_id: str = None, user_id: str = None, **kwargs):
|
|
81
|
+
super().__init__(**kwargs)
|
|
82
|
+
self._tenant_id = tenant_id
|
|
83
|
+
self._user_id = user_id
|
|
84
|
+
self._namespace = user_id
|
|
85
|
+
self._memory = self._initialize_memory()
|
|
86
|
+
write_log_message(f"Mem0Tool 초기화: user_id={self._user_id}, namespace={self._namespace}")
|
|
87
|
+
|
|
88
|
+
def _initialize_memory(self) -> Optional[Memory]:
|
|
89
|
+
"""Memory 인스턴스 초기화 - 에이전트별"""
|
|
90
|
+
if not CONNECTION_STRING:
|
|
91
|
+
return None
|
|
92
|
+
config = {
|
|
93
|
+
"vector_store": {
|
|
94
|
+
"provider": "supabase",
|
|
95
|
+
"config": {
|
|
96
|
+
"connection_string": CONNECTION_STRING,
|
|
97
|
+
"collection_name": "memories",
|
|
98
|
+
"index_method": "hnsw",
|
|
99
|
+
"index_measure": "cosine_distance"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return Memory.from_config(config_dict=config)
|
|
104
|
+
|
|
105
|
+
def _run(self, query: str) -> str:
|
|
106
|
+
"""지식 검색 및 결과 반환 - 에이전트별 메모리에서"""
|
|
107
|
+
if not query:
|
|
108
|
+
return "검색할 쿼리를 입력해주세요."
|
|
109
|
+
if not self._user_id:
|
|
110
|
+
return "개인지식 검색 비활성화: user_id 없음"
|
|
111
|
+
if not self._memory:
|
|
112
|
+
return "mem0 비활성화: DB 연결 정보(DB_*)가 설정되지 않았습니다."
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
write_log_message(f"에이전트별 검색 시작: user_id={self._user_id}, query='{query}'")
|
|
116
|
+
results = self._memory.search(query, user_id=self._user_id)
|
|
117
|
+
hits = results.get("results", [])
|
|
118
|
+
|
|
119
|
+
THRESHOLD = 0.5
|
|
120
|
+
MIN_RESULTS = 5
|
|
121
|
+
hits_sorted = sorted(hits, key=lambda x: x.get("score", 0), reverse=True)
|
|
122
|
+
filtered_hits = [h for h in hits_sorted if h.get("score", 0) >= THRESHOLD]
|
|
123
|
+
if len(filtered_hits) < MIN_RESULTS:
|
|
124
|
+
filtered_hits = hits_sorted[:MIN_RESULTS]
|
|
125
|
+
hits = filtered_hits
|
|
126
|
+
|
|
127
|
+
write_log_message(f"에이전트별 검색 결과: {len(hits)}개 항목 발견")
|
|
128
|
+
|
|
129
|
+
if not hits:
|
|
130
|
+
return f"'{query}'에 대한 개인 지식이 없습니다."
|
|
131
|
+
|
|
132
|
+
return self._format_results(hits)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
handle_application_error("지식검색오류", e, raise_error=False)
|
|
136
|
+
return f"지식검색오류: {e}"
|
|
137
|
+
|
|
138
|
+
def _format_results(self, hits: List[dict]) -> str:
|
|
139
|
+
"""검색 결과 포맷팅"""
|
|
140
|
+
items = []
|
|
141
|
+
for idx, hit in enumerate(hits, start=1):
|
|
142
|
+
memory_text = hit.get("memory", "")
|
|
143
|
+
score = hit.get("score", 0)
|
|
144
|
+
items.append(f"개인지식 {idx} (관련도: {score:.2f})\n{memory_text}")
|
|
145
|
+
|
|
146
|
+
return "\n\n".join(items)
|
|
147
|
+
|
|
148
|
+
# ============================================================================
|
|
149
|
+
# 사내 문서 검색 (memento) 도구
|
|
150
|
+
# ============================================================================
|
|
151
|
+
|
|
152
|
+
class MementoQuerySchema(BaseModel):
|
|
153
|
+
query: str = Field(..., description="검색 키워드 또는 질문")
|
|
154
|
+
|
|
155
|
+
class MementoTool(BaseTool):
|
|
156
|
+
"""사내 문서 검색을 수행하는 도구"""
|
|
157
|
+
name: str = "memento"
|
|
158
|
+
description: str = (
|
|
159
|
+
"🔒 보안 민감한 사내 문서 검색 도구\n\n"
|
|
160
|
+
"저장된 정보:\n"
|
|
161
|
+
"• 보안 민감한 사내 기밀 문서\n"
|
|
162
|
+
"• 대용량 사내 문서 및 정책 자료\n"
|
|
163
|
+
"• 객관적이고 정확한 회사 내부 지식\n"
|
|
164
|
+
"• 업무 프로세스, 규정, 기술 문서\n\n"
|
|
165
|
+
"검색 목적:\n"
|
|
166
|
+
"- 작업지시사항을 올바르게 수행하기 위한 회사 정책/규정/프로세스/매뉴얼 확보\n"
|
|
167
|
+
"- 최신 버전의 표준과 가이드라인 확인\n\n"
|
|
168
|
+
"사용 지침:\n"
|
|
169
|
+
"- 현재 작업/요청과 직접 연결된 문맥을 담아 자연어의 완전한 문장으로 질의하세요.\n"
|
|
170
|
+
"- 문서 제목/버전/담당조직/기간/환경(프로덕션·스테이징·모듈 등) 조건을 명확히 포함하세요.\n"
|
|
171
|
+
"- 약어·정식명칭, 한·영 용어를 함께 사용해 2~3회 재질의하며 누락을 줄이세요.\n"
|
|
172
|
+
"- 처음엔 좁게, 필요 시 점진적으로 범위를 넓혀 검색하세요.\n\n"
|
|
173
|
+
"⚠️ 보안 민감 정보 포함 - 적절한 권한과 용도로만 사용"
|
|
174
|
+
)
|
|
175
|
+
args_schema: Type[MementoQuerySchema] = MementoQuerySchema
|
|
176
|
+
_tenant_id: str = PrivateAttr()
|
|
177
|
+
|
|
178
|
+
def __init__(self, tenant_id: str = "localhost", **kwargs):
|
|
179
|
+
super().__init__(**kwargs)
|
|
180
|
+
self._tenant_id = tenant_id
|
|
181
|
+
write_log_message(f"MementoTool 초기화: tenant_id={self._tenant_id}")
|
|
182
|
+
|
|
183
|
+
def _run(self, query: str) -> str:
|
|
184
|
+
try:
|
|
185
|
+
write_log_message(f"Memento 문서 검색 시작: tenant_id='{self._tenant_id}', query='{query}'")
|
|
186
|
+
response = requests.post(
|
|
187
|
+
"http://memento.process-gpt.io/retrieve",
|
|
188
|
+
json={"query": query, "options": {"tenant_id": self._tenant_id}}
|
|
189
|
+
)
|
|
190
|
+
if response.status_code != 200:
|
|
191
|
+
return f"API 오류: {response.status_code}"
|
|
192
|
+
data = response.json()
|
|
193
|
+
if not data.get("response"):
|
|
194
|
+
return f"테넌트 '{self._tenant_id}'에서 '{query}' 검색 결과가 없습니다."
|
|
195
|
+
results = []
|
|
196
|
+
docs = data.get("response", [])
|
|
197
|
+
write_log_message(f"Memento 검색 결과 개수: {len(docs)}")
|
|
198
|
+
for doc in docs:
|
|
199
|
+
fname = doc.get('metadata', {}).get('file_name', 'unknown')
|
|
200
|
+
idx = doc.get('metadata', {}).get('chunk_index', 'unknown')
|
|
201
|
+
content = doc.get('page_content', '')
|
|
202
|
+
results.append(f"📄 파일: {fname} (청크 #{idx})\n내용: {content}\n---")
|
|
203
|
+
return f"테넌트 '{self._tenant_id}'에서 '{query}' 검색 결과:\n\n" + "\n".join(results)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
handle_application_error("문서검색오류", e, raise_error=False)
|
|
206
|
+
return f"문서검색오류: {e}"
|
|
@@ -6,7 +6,7 @@ import anyio
|
|
|
6
6
|
from mcp.client.stdio import StdioServerParameters
|
|
7
7
|
from crewai_tools import MCPServerAdapter
|
|
8
8
|
from .knowledge_tools import Mem0Tool, MementoTool
|
|
9
|
-
from ..utils.logger import
|
|
9
|
+
from ..utils.logger import write_log_message, handle_application_error
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SafeToolLoader:
|
|
@@ -22,26 +22,27 @@ class SafeToolLoader:
|
|
|
22
22
|
# 외부에서 전달된 MCP 설정 사용 (DB 접근 금지)
|
|
23
23
|
self._mcp_servers = (mcp_config or {}).get('mcpServers', {})
|
|
24
24
|
self.local_tools = ["mem0", "memento", "human_asked"]
|
|
25
|
-
|
|
25
|
+
write_log_message(f"SafeToolLoader 초기화 완료 (tenant_id: {tenant_id}, user_id: {user_id})")
|
|
26
26
|
|
|
27
|
-
def warmup_server(self, server_key: str):
|
|
27
|
+
def warmup_server(self, server_key: str, mcp_config: Optional[Dict] = None):
|
|
28
28
|
"""npx 기반 서버의 패키지를 미리 캐시에 저장해 실제 실행을 빠르게."""
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
servers = (mcp_config or {}).get('mcpServers') or self._mcp_servers or {}
|
|
30
|
+
server_config = servers.get(server_key, {}) if isinstance(servers, dict) else {}
|
|
31
|
+
if not server_config or server_config.get("command") != "npx":
|
|
31
32
|
return
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
if not
|
|
34
|
+
npx_command_path = self._find_npx_command()
|
|
35
|
+
if not npx_command_path:
|
|
35
36
|
return
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
if not (
|
|
38
|
+
arguments_list = server_config.get("args", [])
|
|
39
|
+
if not (arguments_list and arguments_list[0] == "-y"):
|
|
39
40
|
return
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
package_name = arguments_list[1]
|
|
42
43
|
|
|
43
44
|
try:
|
|
44
|
-
subprocess.run([
|
|
45
|
+
subprocess.run([npx_command_path, "-y", package_name, "--help"], capture_output=True, timeout=10, shell=True)
|
|
45
46
|
return
|
|
46
47
|
except subprocess.TimeoutExpired:
|
|
47
48
|
pass
|
|
@@ -49,7 +50,7 @@ class SafeToolLoader:
|
|
|
49
50
|
pass
|
|
50
51
|
|
|
51
52
|
try:
|
|
52
|
-
subprocess.run([
|
|
53
|
+
subprocess.run([npx_command_path, "-y", package_name, "--help"], capture_output=True, timeout=60, shell=True)
|
|
53
54
|
except Exception:
|
|
54
55
|
pass
|
|
55
56
|
|
|
@@ -64,11 +65,11 @@ class SafeToolLoader:
|
|
|
64
65
|
pass
|
|
65
66
|
return "npx"
|
|
66
67
|
|
|
67
|
-
def create_tools_from_names(self, tool_names: List[str]) -> List:
|
|
68
|
+
def create_tools_from_names(self, tool_names: List[str], mcp_config: Optional[Dict] = None) -> List:
|
|
68
69
|
"""tool_names 리스트에서 실제 Tool 객체들 생성"""
|
|
69
70
|
if isinstance(tool_names, str):
|
|
70
71
|
tool_names = [tool_names]
|
|
71
|
-
|
|
72
|
+
write_log_message(f"도구 생성 요청: {tool_names}")
|
|
72
73
|
|
|
73
74
|
tools = []
|
|
74
75
|
|
|
@@ -81,29 +82,29 @@ class SafeToolLoader:
|
|
|
81
82
|
if key in self.local_tools:
|
|
82
83
|
continue
|
|
83
84
|
else:
|
|
84
|
-
self.warmup_server(key)
|
|
85
|
-
tools.extend(self._load_mcp_tool(key))
|
|
85
|
+
self.warmup_server(key, mcp_config)
|
|
86
|
+
tools.extend(self._load_mcp_tool(key, mcp_config))
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
write_log_message(f"총 {len(tools)}개 도구 생성 완료")
|
|
88
89
|
return tools
|
|
89
90
|
|
|
90
91
|
def _load_mem0(self) -> List:
|
|
91
92
|
"""mem0 도구 로드 - 에이전트별 메모리"""
|
|
92
93
|
try:
|
|
93
94
|
if not self.user_id:
|
|
94
|
-
|
|
95
|
+
write_log_message("mem0 도구 로드 생략: user_id 없음")
|
|
95
96
|
return []
|
|
96
97
|
return [Mem0Tool(tenant_id=self.tenant_id, user_id=self.user_id)]
|
|
97
|
-
except Exception as
|
|
98
|
-
|
|
98
|
+
except Exception as error:
|
|
99
|
+
handle_application_error("툴mem0오류", error, raise_error=False)
|
|
99
100
|
return []
|
|
100
101
|
|
|
101
102
|
def _load_memento(self) -> List:
|
|
102
103
|
"""memento 도구 로드"""
|
|
103
104
|
try:
|
|
104
105
|
return [MementoTool(tenant_id=self.tenant_id)]
|
|
105
|
-
except Exception as
|
|
106
|
-
|
|
106
|
+
except Exception as error:
|
|
107
|
+
handle_application_error("툴memento오류", error, raise_error=False)
|
|
107
108
|
return []
|
|
108
109
|
|
|
109
110
|
def _load_human_asked(self) -> List:
|
|
@@ -111,48 +112,49 @@ class SafeToolLoader:
|
|
|
111
112
|
try:
|
|
112
113
|
# 필요한 경우 외부에서 HumanQueryTool을 이 패키지에 추가하여 import하고 리턴하도록 변경 가능
|
|
113
114
|
return []
|
|
114
|
-
except Exception as
|
|
115
|
-
|
|
115
|
+
except Exception as error:
|
|
116
|
+
handle_application_error("툴human오류", error, raise_error=False)
|
|
116
117
|
return []
|
|
117
118
|
|
|
118
|
-
def _load_mcp_tool(self, tool_name: str) -> List:
|
|
119
|
+
def _load_mcp_tool(self, tool_name: str, mcp_config: Optional[Dict] = None) -> List:
|
|
119
120
|
"""MCP 도구 로드 (timeout & retry 지원)"""
|
|
120
121
|
self._apply_anyio_patch()
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
if
|
|
123
|
+
servers = (mcp_config or {}).get('mcpServers') or self._mcp_servers or {}
|
|
124
|
+
server_config = servers.get(tool_name, {}) if isinstance(servers, dict) else {}
|
|
125
|
+
if not server_config:
|
|
124
126
|
return []
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
environment_variables = os.environ.copy()
|
|
129
|
+
environment_variables.update(server_config.get("env", {}))
|
|
130
|
+
timeout_seconds = server_config.get("timeout", 40)
|
|
129
131
|
|
|
130
132
|
max_retries = 2
|
|
131
133
|
retry_delay = 5
|
|
132
134
|
|
|
133
135
|
for attempt in range(1, max_retries + 1):
|
|
134
136
|
try:
|
|
135
|
-
cmd =
|
|
137
|
+
cmd = server_config["command"]
|
|
136
138
|
if cmd == "npx":
|
|
137
139
|
cmd = self._find_npx_command() or cmd
|
|
138
140
|
|
|
139
141
|
params = StdioServerParameters(
|
|
140
142
|
command=cmd,
|
|
141
|
-
args=
|
|
142
|
-
env=
|
|
143
|
-
timeout=
|
|
143
|
+
args=server_config.get("args", []),
|
|
144
|
+
env=environment_variables,
|
|
145
|
+
timeout=timeout_seconds
|
|
144
146
|
)
|
|
145
147
|
|
|
146
148
|
adapter = MCPServerAdapter(params)
|
|
147
149
|
SafeToolLoader.adapters.append(adapter)
|
|
148
|
-
|
|
150
|
+
write_log_message(f"{tool_name} MCP 로드 성공 (툴 {len(adapter.tools)}개): {[tool.name for tool in adapter.tools]}")
|
|
149
151
|
return adapter.tools
|
|
150
152
|
|
|
151
153
|
except Exception as e:
|
|
152
154
|
if attempt < max_retries:
|
|
153
155
|
time.sleep(retry_delay)
|
|
154
156
|
else:
|
|
155
|
-
|
|
157
|
+
handle_application_error(f"툴{tool_name}오류", e, raise_error=False)
|
|
156
158
|
return []
|
|
157
159
|
|
|
158
160
|
def _apply_anyio_patch(self):
|
|
@@ -171,21 +173,13 @@ class SafeToolLoader:
|
|
|
171
173
|
anyio._core._subprocesses.open_process = patched_open_process
|
|
172
174
|
SafeToolLoader.ANYIO_PATCHED = True
|
|
173
175
|
|
|
174
|
-
def _get_mcp_config(self, tool_name: str) -> dict:
|
|
175
|
-
"""전달받은 mcp_config에서 툴별 설정을 조회."""
|
|
176
|
-
try:
|
|
177
|
-
return self._mcp_servers.get(tool_name, {}) if self._mcp_servers else {}
|
|
178
|
-
except Exception as e:
|
|
179
|
-
handle_error("툴설정오류", e, raise_error=False)
|
|
180
|
-
return {}
|
|
181
|
-
|
|
182
176
|
@classmethod
|
|
183
177
|
def shutdown_all_adapters(cls):
|
|
184
178
|
"""모든 MCPServerAdapter 연결 종료"""
|
|
185
179
|
for adapter in cls.adapters:
|
|
186
180
|
try:
|
|
187
181
|
adapter.stop()
|
|
188
|
-
except Exception as
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
except Exception as error:
|
|
183
|
+
handle_application_error("툴종료오류", error, raise_error=False)
|
|
184
|
+
write_log_message("모든 MCPServerAdapter 연결 종료 완료")
|
|
191
185
|
cls.adapters.clear()
|
|
@@ -9,9 +9,10 @@ proc_id_var: ContextVar[Optional[str]] = ContextVar("proc_id", default=None)
|
|
|
9
9
|
crew_type_var: ContextVar[Optional[str]] = ContextVar("crew_type", default=None)
|
|
10
10
|
form_key_var: ContextVar[Optional[str]] = ContextVar("form_key", default=None)
|
|
11
11
|
form_id_var: ContextVar[Optional[str]] = ContextVar("form_id", default=None)
|
|
12
|
+
all_users_var: ContextVar[Optional[str]] = ContextVar("all_users", default=None)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] = None, crew_type: Optional[str] = None, form_key: Optional[str] = None, form_id: Optional[str] = None) -> None:
|
|
15
|
+
def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] = None, crew_type: Optional[str] = None, form_key: Optional[str] = None, form_id: Optional[str] = None, all_users: Optional[str] = None) -> None:
|
|
15
16
|
if todo_id is not None:
|
|
16
17
|
todo_id_var.set(todo_id)
|
|
17
18
|
if proc_inst_id is not None:
|
|
@@ -22,6 +23,8 @@ def set_context(*, todo_id: Optional[str] = None, proc_inst_id: Optional[str] =
|
|
|
22
23
|
form_key_var.set(form_key)
|
|
23
24
|
if form_id is not None:
|
|
24
25
|
form_id_var.set(form_id)
|
|
26
|
+
if all_users is not None:
|
|
27
|
+
all_users_var.set(all_users)
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def reset_context() -> None:
|
|
@@ -30,4 +33,5 @@ def reset_context() -> None:
|
|
|
30
33
|
crew_type_var.set(None)
|
|
31
34
|
form_key_var.set(None)
|
|
32
35
|
form_id_var.set(None)
|
|
36
|
+
all_users_var.set(None)
|
|
33
37
|
|