kcsc-mcp / src /mcp_processor.py
nicefree19's picture
Upload 105 files
0ee3c92 verified
Raw
History Blame Contribute Delete
15.7 kB
"""
Model Context Protocol(MCP) ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ
Claude์™€์˜ ํ†ต์‹ ์„ ์œ„ํ•œ MCP ํ”„๋กœํ† ์ฝœ ์ฒ˜๋ฆฌ ํด๋ž˜์Šค
"""
import os
import json
import logging
import uuid
from datetime import datetime
from typing import Dict, List, Any, Optional, Union
import mcp
from src import config
from src.utils import (
MCPError,
DataProcessingError,
generate_unique_id,
save_json_data,
merge_metadata
)
logger = config.setup_logger("mcp_processor")
class MCPProcessor:
"""
Claude์™€์˜ ํ†ต์‹ ์„ ์œ„ํ•œ MCP ํ”„๋กœํ† ์ฝœ ์ฒ˜๋ฆฌ๊ธฐ
MCP ์ŠคํŽ™์— ๋งž๊ฒŒ ์ปจํ…์ŠคํŠธ๋ฅผ ์ค€๋น„ํ•˜๊ณ  Claude์™€ ํ†ต์‹ ํ•˜๋Š” ๊ธฐ๋Šฅ ์ œ๊ณต
"""
_instance = None
def __new__(cls, *args, **kwargs):
"""์‹ฑ๊ธ€ํ„ด ํŒจํ„ด ๊ตฌํ˜„"""
if cls._instance is None:
cls._instance = super(MCPProcessor, cls).__new__(cls)
return cls._instance
def __init__(self, model_name: str = None, api_key: str = None):
"""
MCP ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™”
Args:
model_name: ์‚ฌ์šฉํ•  Claude ๋ชจ๋ธ๋ช…
api_key: API ํ‚ค
"""
# ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋œ ๊ฒฝ์šฐ ์ค‘๋ณต ์ดˆ๊ธฐํ™” ๋ฐฉ์ง€
if hasattr(self, 'initialized') and self.initialized:
return
# MCP SDK ํ™˜๊ฒฝ ์„ค์ •
self.model_name = model_name or config.MCP_MODEL_NAME
self.api_key = api_key or config.MCP_API_KEY
try:
# MCP ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™”
self.client = mcp.Client(
model=self.model_name,
api_key=self.api_key if self.api_key else None # API ํ‚ค๊ฐ€ ์—†์œผ๋ฉด None ์ „๋‹ฌ
)
self.conversations = {} # ๋Œ€ํ™” ๋‚ด์šฉ ์บ์‹œ
self.data_dir = config.DATA_DIR
self.initialized = True
logger.info(f"MCP ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™”๋จ (๋ชจ๋ธ: {self.model_name})")
except Exception as e:
logger.error(f"MCP ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {str(e)}")
raise MCPError(f"MCP ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {str(e)}", "init")
def process_query(
self,
query: str,
context_data: Optional[Union[List[Dict[str, Any]], Dict[str, Any]]] = None,
conversation_id: Optional[str] = None,
system_prompt: Optional[str] = None,
temperature: float = 0.2,
max_tokens: int = 2000
) -> Dict[str, Any]:
"""
MCP๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์‘๋‹ต ์ƒ์„ฑ
Args:
query: ์‚ฌ์šฉ์ž ์ฟผ๋ฆฌ
context_data: ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ
conversation_id: ๋Œ€ํ™” ID
system_prompt: ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ
temperature: ์ƒ์„ฑ ์˜จ๋„(์ฐฝ์˜์„ฑ)
max_tokens: ์ตœ๋Œ€ ์ƒ์„ฑ ํ† ํฐ ์ˆ˜
Returns:
์‘๋‹ต ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋”•์…”๋„ˆ๋ฆฌ
Raises:
MCPError: MCP ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ
"""
if not query:
raise MCPError("์ฟผ๋ฆฌ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.", "empty_query")
logger.info(f"MCP ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์ค‘: {query[:50]}...")
# ์ปจํ…์ŠคํŠธ ์ฒ˜๋ฆฌ
context = None
if context_data:
context = self._prepare_context(context_data)
logger.debug(f"์ปจํ…์ŠคํŠธ ์ค€๋น„ ์™„๋ฃŒ: {len(context)}๊ฐœ ํ•ญ๋ชฉ")
# ๋Œ€ํ™” ID ์ฒ˜๋ฆฌ
if not conversation_id:
conversation_id = generate_unique_id("conv")
logger.debug(f"์ƒˆ ๋Œ€ํ™” ID ์ƒ์„ฑ: {conversation_id}")
# ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ค€๋น„
if system_prompt:
# ์‚ฌ์šฉ์ž ์ œ๊ณต ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ
pass
else:
# ๊ธฐ๋ณธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์„ค์ •
system_prompt = self._get_default_system_prompt()
try:
# MCP๋ฅผ ํ†ตํ•œ ์‘๋‹ต ์ƒ์„ฑ
response = self.client.query(
message=query,
context=context,
conversation_id=conversation_id,
system_prompt=system_prompt,
parameters={
"temperature": temperature,
"max_tokens": max_tokens
}
)
logger.info(f"MCP ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ: {len(response.content)} ์ž")
# ๋Œ€ํ™” ์ด๋ ฅ ์บ์‹ฑ
self._update_conversation_cache(conversation_id, query, response.content, context)
# ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋Œ€ํ™” ์ €์žฅ
# self.save_conversation(conversation_id)
# ์‘๋‹ต ๋ฐ ์‚ฌ์šฉ๋œ ์ปจํ…์ŠคํŠธ ๋ฐ˜ํ™˜
result = {
"content": response.content,
"conversation_id": response.conversation_id,
"used_context": getattr(response, 'used_context', None),
"query": query,
"timestamp": datetime.now().isoformat()
}
return result
except Exception as e:
logger.error(f"MCP ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise MCPError(f"MCP ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", "query_processing")
def _prepare_context(
self,
context_data: Union[List[Dict[str, Any]], Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
MCP์— ์ „๋‹ฌํ•  ์ปจํ…์ŠคํŠธ ์ค€๋น„
Args:
context_data: ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ
Returns:
MCP ํ˜•์‹์˜ ์ปจํ…์ŠคํŠธ ๋ฆฌ์ŠคํŠธ
"""
if isinstance(context_data, list):
# ์ด๋ฏธ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ์ „๋‹ฌ๋œ ๊ฒฝ์šฐ
return [self._format_context_item(item) for item in context_data]
elif isinstance(context_data, dict):
# ๋‹จ์ผ ์•„์ดํ…œ์ด ๋”•์…”๋„ˆ๋ฆฌ๋กœ ์ „๋‹ฌ๋œ ๊ฒฝ์šฐ
return [self._format_context_item(context_data)]
else:
logger.error(f"์ง€์›๋˜์ง€ ์•Š๋Š” ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ํ˜•์‹: {type(context_data)}")
raise MCPError(
f"์ง€์›๋˜์ง€ ์•Š๋Š” ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ํ˜•์‹: {type(context_data)}",
"invalid_context_format"
)
def _format_context_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
"""
์ปจํ…์ŠคํŠธ ์•„์ดํ…œ์„ MCP ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
Args:
item: ์›๋ณธ ์ปจํ…์ŠคํŠธ ์•„์ดํ…œ
Returns:
MCP ํ˜•์‹์˜ ์ปจํ…์ŠคํŠธ ์•„์ดํ…œ
"""
# ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ ('text' ๋˜๋Š” 'content')
raw_text = item.get("text") or item.get("content")
if not raw_text:
raise MCPError("์ปจํ…์ŠคํŠธ ์•„์ดํ…œ์— 'text' ๋˜๋Š” 'content' ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", "missing_context_content")
# ๊ณ ์œ  ID ์ƒ์„ฑ ๋˜๋Š” ์‚ฌ์šฉ
ctx_id = item.get("id", generate_unique_id("ctx"))
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”์ถœ
metadata = item.get("metadata", {})
source_code = metadata.get("code", "์•Œ ์ˆ˜ ์—†์Œ")
relevance = item.get("relevance", metadata.get("relevance", 0))
# XML ํƒœ๊ทธ๋กœ ์ปจํ…์ŠคํŠธ ๋ž˜ํ•‘
xml_content = f"""<evidence source="{source_code}" relevance="{relevance:.4f}">
{raw_text}
</evidence>"""
# ๊ธฐ๋ณธ ํ•„๋“œ ์„ค์ • (MCP ํ‘œ์ค€ ํ˜•์‹)
formatted = {
"id": ctx_id,
"content": xml_content
}
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
if "metadata" in item:
formatted["metadata"] = item["metadata"]
# ์ƒ์„ฑ ์‹œ๊ฐ„ ์ถ”๊ฐ€ (์—†๋Š” ๊ฒฝ์šฐ)
if "created_at" not in formatted["metadata"]:
formatted["metadata"]["created_at"] = datetime.now().isoformat()
else:
# ๊ธฐ๋ณธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
formatted["metadata"] = {
"source": "KCSC",
"created_at": datetime.now().isoformat()
}
return formatted
def _get_default_system_prompt(self) -> str:
"""๊ธฐ๋ณธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ"""
return """ํ•œ๊ตญ๊ฑด์„ค๊ธฐ์ค€์„ผํ„ฐ(KCSC)์˜ ๊ฑด์„ค๊ธฐ์ค€ ์ •๋ณด์— ๋Œ€ํ•œ ์งˆ์˜์‘๋‹ต์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
์ฃผ์–ด์ง„ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ •ํ™•ํ•˜๊ณ  ๊ฐ๊ด€์ ์ธ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜์„ธ์š”.
๊ฑด์„ค๊ธฐ์ค€์— ๋ช…์‹œ๋œ ๋‚ด์šฉ์„ ์šฐ์„ ์ ์œผ๋กœ ์ฐธ๊ณ ํ•˜๋˜, ๋ช…ํ™•ํ•œ ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” ์ด๋ฅผ ์†”์งํ•˜๊ฒŒ ๋ฐํžˆ๊ณ 
์ผ๋ฐ˜์ ์ธ ๊ฑด์ถ•๊ตฌ์กฐ ์ง€์‹์„ ๋ฐ”ํƒ•์œผ๋กœ ๋‹ต๋ณ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋‹ต๋ณ€ ์‹œ ์ฐธ๊ณ ํ•  ์‚ฌํ•ญ:
1. ์ธ์šฉ๋œ ๊ฑด์„ค๊ธฐ์ค€ ์ฝ”๋“œ์™€ ์ถœ์ฒ˜๋ฅผ ๋ช…ํ™•ํžˆ ๊ธฐ์žฌํ•˜์„ธ์š”.
2. ๊ด€๋ จ ์ˆ˜์‹์ด๋‚˜ ๋„ํ‘œ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ ์ ˆํžˆ ํฌ๋งทํŒ…ํ•˜์—ฌ ์ œ๊ณตํ•˜์„ธ์š”.
3. ๋ถˆ์™„์ „ํ•˜๊ฑฐ๋‚˜ ๋ชจํ˜ธํ•œ ์ •๋ณด์— ๋Œ€ํ•ด์„œ๋Š” ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๊ฑฐ๋‚˜ ์ œํ•œ์‚ฌํ•ญ์„ ๋ช…์‹œํ•˜์„ธ์š”.
4. ๊ฐ€๋Šฅํ•œ ํ•œ ๊ด€๋ จ ๋ฒ•๊ทœ, ๊ทœ์ •, ํ‘œ์ค€๊ณผ ๊ฐ™์€ ๊ณต์‹ ์ •๋ณด๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
5. ํŠน์ • ์„ค๊ณ„ ๋ฌธ์ œ์— ๋Œ€ํ•œ ์ตœ์ข… ์ฑ…์ž„์€ ํ•ญ์ƒ ๊ณต์ธ๋œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์žˆ์Œ์„ ๋ช…์‹œํ•˜์„ธ์š”.
ํ•œ๊ตญ์–ด๋กœ ๋‹ต๋ณ€ํ•˜๋˜, ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ „๋ฌธ ์šฉ์–ด๋Š” ํ•œ๊ธ€๊ณผ ์˜๋ฌธ์„ ๋ณ‘๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."""
def _update_conversation_cache(
self,
conversation_id: str,
query: str,
response: str,
context: Optional[List[Dict[str, Any]]] = None
):
"""
๋Œ€ํ™” ์ด๋ ฅ ์บ์‹œ ์—…๋ฐ์ดํŠธ
Args:
conversation_id: ๋Œ€ํ™” ID
query: ์‚ฌ์šฉ์ž ์ฟผ๋ฆฌ
response: AI ์‘๋‹ต
context: ์‚ฌ์šฉ๋œ ์ปจํ…์ŠคํŠธ
"""
if conversation_id not in self.conversations:
self.conversations[conversation_id] = {
"id": conversation_id,
"messages": [],
"contexts": [],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
# ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
self.conversations[conversation_id]["messages"].append({
"role": "user",
"content": query,
"timestamp": datetime.now().isoformat()
})
self.conversations[conversation_id]["messages"].append({
"role": "assistant",
"content": response,
"timestamp": datetime.now().isoformat()
})
# ์ปจํ…์ŠคํŠธ ์ถ”๊ฐ€ (์ค‘๋ณต ๋ฐฉ์ง€)
if context:
for ctx in context:
ctx_id = ctx.get("id")
if ctx_id:
# ์ด๋ฏธ ์žˆ๋Š” ์ปจํ…์ŠคํŠธ์ธ์ง€ ํ™•์ธ
existing_ctx = next(
(c for c in self.conversations[conversation_id]["contexts"] if c.get("id") == ctx_id),
None
)
if not existing_ctx:
self.conversations[conversation_id]["contexts"].append(ctx)
# ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ๊ฐฑ์‹ 
self.conversations[conversation_id]["updated_at"] = datetime.now().isoformat()
def get_conversation_history(self, conversation_id: str) -> Optional[Dict[str, Any]]:
"""
๋Œ€ํ™” ์ด๋ ฅ ์กฐํšŒ
Args:
conversation_id: ๋Œ€ํ™” ID
Returns:
๋Œ€ํ™” ์ด๋ ฅ ๋˜๋Š” None (์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ)
"""
# ์บ์‹œ์—์„œ ๋จผ์ € ์กฐํšŒ
if conversation_id in self.conversations:
return self.conversations[conversation_id]
# MCP SDK์˜ ๋Œ€ํ™” ์ด๋ ฅ ์กฐํšŒ ๊ธฐ๋Šฅ ์‚ฌ์šฉ
try:
logger.info(f"MCP์—์„œ ๋Œ€ํ™” ์ด๋ ฅ ์กฐํšŒ ์ค‘: {conversation_id}")
history = self.client.get_conversation(conversation_id)
# ์บ์‹œ ์—…๋ฐ์ดํŠธ
self.conversations[conversation_id] = history
return history
except Exception as e:
logger.error(f"๋Œ€ํ™” ์ด๋ ฅ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
return None
def save_conversation(
self,
conversation_id: str,
file_prefix: str = "conversation",
include_context: bool = True
) -> Optional[str]:
"""
๋Œ€ํ™” ๋‚ด์šฉ ์ €์žฅ
Args:
conversation_id: ์ €์žฅํ•  ๋Œ€ํ™” ID
file_prefix: ํŒŒ์ผ๋ช… ์ ‘๋‘์‚ฌ
include_context: ์ปจํ…์ŠคํŠธ ํฌํ•จ ์—ฌ๋ถ€
Returns:
์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ ๋˜๋Š” None (์‹คํŒจ ์‹œ)
"""
if not conversation_id:
logger.warning("์ €์žฅํ•  ๋Œ€ํ™” ID๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return None
try:
# ๋Œ€ํ™” ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
conversation = self.get_conversation_history(conversation_id)
if not conversation:
logger.warning(f"๋Œ€ํ™” ID {conversation_id}์— ํ•ด๋‹นํ•˜๋Š” ๋Œ€ํ™”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
return None
# ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ๋ง (์„ ํƒ์ )
if not include_context and "contexts" in conversation:
conversation = conversation.copy()
conversation.pop("contexts", None)
# ํŒŒ์ผ์— ์ €์žฅ
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{file_prefix}_{timestamp}.json"
filepath = save_json_data(conversation, filename, self.data_dir)
logger.info(f"๋Œ€ํ™” ๋‚ด์šฉ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
return filepath
except Exception as e:
logger.error(f"๋Œ€ํ™” ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
return None
def delete_conversation(self, conversation_id: str) -> bool:
"""
๋Œ€ํ™” ์‚ญ์ œ
Args:
conversation_id: ์‚ญ์ œํ•  ๋Œ€ํ™” ID
Returns:
์„ฑ๊ณต ์—ฌ๋ถ€
"""
if not conversation_id:
logger.warning("์‚ญ์ œํ•  ๋Œ€ํ™” ID๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return False
try:
# ์บ์‹œ์—์„œ ์‚ญ์ œ
if conversation_id in self.conversations:
del self.conversations[conversation_id]
# MCP ํด๋ผ์ด์–ธํŠธ์—์„œ ์‚ญ์ œ (API ์ง€์› ์‹œ)
try:
self.client.delete_conversation(conversation_id)
except Exception as e:
logger.warning(f"MCP ํด๋ผ์ด์–ธํŠธ์—์„œ ๋Œ€ํ™” ์‚ญ์ œ ์‹คํŒจ (๊ธฐ๋Šฅ ๋ฏธ์ง€์›): {str(e)}")
logger.info(f"๋Œ€ํ™” ID {conversation_id} ์‚ญ์ œ ์™„๋ฃŒ")
return True
except Exception as e:
logger.error(f"๋Œ€ํ™” ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
return False
# ์‹คํ–‰ ์˜ˆ์‹œ
if __name__ == "__main__":
# ์„ค์ • ์ดˆ๊ธฐํ™”
config.init()
# MCP ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™”
processor = MCPProcessor()
# ํ…Œ์ŠคํŠธ ์ฟผ๋ฆฌ
test_query = "์ฝ˜ํฌ๋ฆฌํŠธ ๊ธฐ๋‘ฅ์˜ ์ตœ์†Œ ์ฒ ๊ทผ๋น„๋Š” ์–ผ๋งˆ์ธ๊ฐ€์š”?"
# ํ…Œ์ŠคํŠธ ์ปจํ…์ŠคํŠธ
test_context = [
{
"id": "ctx-1",
"content": "KDS 14 20 01 ์ฝ˜ํฌ๋ฆฌํŠธ๊ตฌ์กฐ ์„ค๊ณ„๊ธฐ์ค€์— ๋”ฐ๋ฅด๋ฉด, ๊ธฐ๋‘ฅ์˜ ์ตœ์†Œ ์ฒ ๊ทผ๋น„๋Š” ๋‹จ๋ฉด์ ์˜ 1%์ด๋‹ค.",
"metadata": {
"source": "KDS 14 20 01",
"title": "์ฝ˜ํฌ๋ฆฌํŠธ๊ตฌ์กฐ ์„ค๊ณ„๊ธฐ์ค€",
"relevance": 0.95
}
}
]
# ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ
result = processor.process_query(test_query, test_context)
print(result["content"])