Spaces:
Sleeping
Sleeping
Commit ·
b863252
1
Parent(s): a57d85f
long response handling
Browse files- config/default_responses.py +0 -16
- config/long_fields_config.json +16 -0
- config/prompt_templates.py +9 -11
- config/settings.py +7 -1
- scripts/demo_conversation_history.py +0 -97
- scripts/diagnose_issues.py +0 -189
- scripts/migrate_to_v2.py +0 -269
- scripts/test_context_aware_selection.py +0 -207
- scripts/test_full_execution.py +0 -184
- scripts/test_tool_selection.py +0 -156
- src/application/services/specs_service.py +0 -486
- src/infrastructure/logging/logger.py +0 -164
- src/infrastructure/providers/long_field_manager.py +247 -0
- src/infrastructure/providers/response_filter.py +19 -14
- src/presentation/gradio_interface.py +18 -1
config/default_responses.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
# config/default_responses.py
|
| 2 |
-
|
| 3 |
-
default_help_response = (
|
| 4 |
-
"🤖 I'm a Topcoder MCP Agent! I can:\n"
|
| 5 |
-
"- Show active challenges\n"
|
| 6 |
-
"- Fetch member profiles (with handle)\n"
|
| 7 |
-
"- Answer questions about Topcoder processes\n\n"
|
| 8 |
-
"I won't be able to help with general knowledge or personal tasks."
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
help_triggers = [
|
| 12 |
-
"what can you do?",
|
| 13 |
-
"what can you do for me",
|
| 14 |
-
"help",
|
| 15 |
-
"how can you help me?",
|
| 16 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/long_fields_config.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_note": "This file is human- and LLM-editable. It tracks long field paths per tool/resource.",
|
| 3 |
+
"tools": {
|
| 4 |
+
"query-tc-skills": [
|
| 5 |
+
"description"
|
| 6 |
+
],
|
| 7 |
+
"query-tc-challenges": [
|
| 8 |
+
"description"
|
| 9 |
+
]
|
| 10 |
+
},
|
| 11 |
+
"resources": {
|
| 12 |
+
"Member_V6_API_Swagger": [
|
| 13 |
+
"skills"
|
| 14 |
+
]
|
| 15 |
+
}
|
| 16 |
+
}
|
config/prompt_templates.py
CHANGED
|
@@ -87,22 +87,20 @@ Valid parameter names to extract:
|
|
| 87 |
Respond ONLY with a JSON object containing the extracted parameters:
|
| 88 |
{{"params": {{"param1": "value1", "param2": "value2"}} }}"""
|
| 89 |
|
| 90 |
-
GRADIO_RESPONSE_SUMMARIZATION = """You are a Topcoder assistant. Provide
|
| 91 |
|
| 92 |
-
|
| 93 |
{context}
|
| 94 |
|
| 95 |
-
|
| 96 |
{filtered_data}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
Write a clear, helpful response based on the actual data provided."""
|
| 106 |
|
| 107 |
# Tool decision prompt template
|
| 108 |
TOOL_DECISION_PROMPT = """You are an assistant that ONLY works with Topcoder's Member Communication Platform (MCP).
|
|
|
|
| 87 |
Respond ONLY with a JSON object containing the extracted parameters:
|
| 88 |
{{"params": {{"param1": "value1", "param2": "value2"}} }}"""
|
| 89 |
|
| 90 |
+
GRADIO_RESPONSE_SUMMARIZATION = """You are a Topcoder assistant. Provide clear, helpful responses based on the actual data.
|
| 91 |
|
| 92 |
+
USER QUERY: {user_message}
|
| 93 |
{context}
|
| 94 |
|
| 95 |
+
RESPONSE DATA:
|
| 96 |
{filtered_data}
|
| 97 |
|
| 98 |
+
INSTRUCTIONS:
|
| 99 |
+
- Give a natural, conversational response
|
| 100 |
+
- If results exist, summarize what was found
|
| 101 |
+
- If no results, say "No results found"
|
| 102 |
+
- If some fields were omitted due to length, simply ask: "Would you like to see more details about [field names]?"
|
| 103 |
+
- Keep it simple and helpful - don't expose technical details"""
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# Tool decision prompt template
|
| 106 |
TOOL_DECISION_PROMPT = """You are an assistant that ONLY works with Topcoder's Member Communication Platform (MCP).
|
config/settings.py
CHANGED
|
@@ -30,4 +30,10 @@ EXCLUDE_AUTH_REQUIRED_ENDPOINTS = os.getenv("EXCLUDE_AUTH_REQUIRED_ENDPOINTS", "
|
|
| 30 |
EXCLUDE_401_RESPONSES = os.getenv("EXCLUDE_401_RESPONSES", "true").lower() == "true"
|
| 31 |
|
| 32 |
# Set to True to be more strict and exclude endpoints that mention authentication in descriptions
|
| 33 |
-
STRICT_AUTH_FILTERING = os.getenv("STRICT_AUTH_FILTERING", "false").lower() == "true"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
EXCLUDE_401_RESPONSES = os.getenv("EXCLUDE_401_RESPONSES", "true").lower() == "true"
|
| 31 |
|
| 32 |
# Set to True to be more strict and exclude endpoints that mention authentication in descriptions
|
| 33 |
+
STRICT_AUTH_FILTERING = os.getenv("STRICT_AUTH_FILTERING", "false").lower() == "true"
|
| 34 |
+
|
| 35 |
+
# Long-field detection thresholds
|
| 36 |
+
# Tune these to control when fields are treated as long and omitted by default
|
| 37 |
+
LONG_FIELD_STRING_MAX_CHARS = int(os.getenv("LONG_FIELD_STRING_MAX_CHARS", "500"))
|
| 38 |
+
LONG_FIELD_ARRAY_MAX_ITEMS = int(os.getenv("LONG_FIELD_ARRAY_MAX_ITEMS", "25"))
|
| 39 |
+
LONG_FIELD_ARRAY_MAX_BYTES = int(os.getenv("LONG_FIELD_ARRAY_MAX_BYTES", "10000"))
|
scripts/demo_conversation_history.py
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Demo script for conversation history functionality.
|
| 4 |
-
This script demonstrates how the conversation history system works.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import asyncio
|
| 8 |
-
import sys
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
# Add src to path so we can import modules
|
| 12 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 13 |
-
|
| 14 |
-
from src.domain.services.conversation_history import ConversationHistoryService
|
| 15 |
-
from src.domain.services.session_manager import SessionManager
|
| 16 |
-
|
| 17 |
-
async def demo_conversation_history():
|
| 18 |
-
"""Demonstrate conversation history functionality."""
|
| 19 |
-
|
| 20 |
-
print("🧠 Conversation History Demo")
|
| 21 |
-
print("=" * 50)
|
| 22 |
-
|
| 23 |
-
# Initialize services
|
| 24 |
-
history_service = ConversationHistoryService(default_max_history=10, default_max_tokens=2000)
|
| 25 |
-
session_manager = SessionManager(history_service)
|
| 26 |
-
|
| 27 |
-
# Create a session
|
| 28 |
-
session_id = session_manager.create_session()
|
| 29 |
-
print(f"📝 Created session: {session_id}")
|
| 30 |
-
|
| 31 |
-
# Simulate a conversation about challenges
|
| 32 |
-
conversation = [
|
| 33 |
-
("user", "Show me active challenges"),
|
| 34 |
-
("assistant", "I found 15 active challenges. Here are some highlights: AI Challenge #123, Web Development #456, and Mobile App #789."),
|
| 35 |
-
("user", "What about AI challenges specifically?"),
|
| 36 |
-
("assistant", "Looking at the active challenges, here are the AI-specific ones: AI Challenge #123 (Machine Learning), AI Challenge #124 (Computer Vision), and AI Challenge #125 (NLP)."),
|
| 37 |
-
("user", "Tell me more about the Machine Learning challenge"),
|
| 38 |
-
("assistant", "AI Challenge #123 (Machine Learning) has a prize pool of $10,000, deadline in 2 weeks, and focuses on developing an image classification model."),
|
| 39 |
-
("user", "What skills are needed for this challenge?"),
|
| 40 |
-
("assistant", "For AI Challenge #123, you'll need: Python, TensorFlow/PyTorch, Computer Vision, Machine Learning, and experience with image classification algorithms."),
|
| 41 |
-
("user", "Are there any similar challenges?"),
|
| 42 |
-
("assistant", "Yes, there are similar challenges: AI Challenge #124 (Computer Vision) and AI Challenge #125 (NLP) both require similar AI/ML skills but focus on different domains.")
|
| 43 |
-
]
|
| 44 |
-
|
| 45 |
-
print("\n💬 Simulating conversation...")
|
| 46 |
-
|
| 47 |
-
# Add messages to history
|
| 48 |
-
for i, (role, content) in enumerate(conversation):
|
| 49 |
-
history_service.add_message(session_id, role, content)
|
| 50 |
-
print(f" {role.upper()}: {content[:50]}{'...' if len(content) > 50 else ''}")
|
| 51 |
-
|
| 52 |
-
# Show history after each message
|
| 53 |
-
if i % 2 == 1: # After each exchange
|
| 54 |
-
current_history = history_service.get_conversation_history(session_id)
|
| 55 |
-
print(f" 📚 History length: {len(current_history)} messages")
|
| 56 |
-
|
| 57 |
-
# Show final history
|
| 58 |
-
print("\n📋 Final Conversation History:")
|
| 59 |
-
final_history = history_service.get_conversation_history(session_id)
|
| 60 |
-
for i, msg in enumerate(final_history):
|
| 61 |
-
role_icon = "👤" if msg["role"] == "user" else "🤖"
|
| 62 |
-
print(f" {role_icon} {msg['role'].upper()}: {msg['content'][:60]}{'...' if len(msg['content']) > 60 else ''}")
|
| 63 |
-
|
| 64 |
-
# Show session info
|
| 65 |
-
print("\n📊 Session Information:")
|
| 66 |
-
session_info = history_service.get_session_info(session_id)
|
| 67 |
-
for key, value in session_info.items():
|
| 68 |
-
if key != "session_id": # Skip the long session ID
|
| 69 |
-
print(f" {key}: {value}")
|
| 70 |
-
|
| 71 |
-
# Show recent messages
|
| 72 |
-
print("\n🕒 Recent Messages (last 3):")
|
| 73 |
-
recent = history_service.get_recent_messages(session_id, count=3)
|
| 74 |
-
for msg in recent:
|
| 75 |
-
role_icon = "👤" if msg["role"] == "user" else "🤖"
|
| 76 |
-
print(f" {role_icon} {msg['role'].upper()}: {msg['content'][:50]}{'...' if len(msg['content']) > 50 else ''}")
|
| 77 |
-
|
| 78 |
-
# Test history limits
|
| 79 |
-
print("\n🧪 Testing History Limits:")
|
| 80 |
-
print(" Adding 15 more messages to test sliding window...")
|
| 81 |
-
|
| 82 |
-
for i in range(15):
|
| 83 |
-
history_service.add_message(session_id, "user", f"Test message {i}")
|
| 84 |
-
history_service.add_message(session_id, "assistant", f"Test response {i}")
|
| 85 |
-
|
| 86 |
-
final_history = history_service.get_conversation_history(session_id)
|
| 87 |
-
print(f" Final history length: {len(final_history)} (should be <= 10)")
|
| 88 |
-
print(f" ✅ History limit working: {len(final_history) <= 10}")
|
| 89 |
-
|
| 90 |
-
# Cleanup
|
| 91 |
-
session_manager.remove_session(session_id)
|
| 92 |
-
print(f"\n🧹 Cleaned up session: {session_id}")
|
| 93 |
-
|
| 94 |
-
print("\n✅ Demo completed successfully!")
|
| 95 |
-
|
| 96 |
-
if __name__ == "__main__":
|
| 97 |
-
asyncio.run(demo_conversation_history())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/diagnose_issues.py
DELETED
|
@@ -1,189 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Diagnostic script to identify issues with tool selection and execution.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import asyncio
|
| 7 |
-
import sys
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
|
| 10 |
-
# Add src to path so we can import modules
|
| 11 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 12 |
-
|
| 13 |
-
from src.infrastructure.providers.llm_provider import LLMClient
|
| 14 |
-
from src.application.services.prompt_service import PromptBuilder
|
| 15 |
-
from src.application.services.tool_service import ToolExecutor
|
| 16 |
-
from src.mcp.compact_utils import CompactSpecsUtils
|
| 17 |
-
from src.mcp.client.transport import load_server_config, StreamableHttpMCPClient
|
| 18 |
-
|
| 19 |
-
async def diagnose_components():
|
| 20 |
-
"""Diagnose each component of the system."""
|
| 21 |
-
|
| 22 |
-
print("🔍 System Diagnostics")
|
| 23 |
-
print("=" * 50)
|
| 24 |
-
|
| 25 |
-
# 1. Check compact specs
|
| 26 |
-
print("\n1. 📋 Checking Compact Specs...")
|
| 27 |
-
compact_utils = CompactSpecsUtils()
|
| 28 |
-
|
| 29 |
-
working_tools = compact_utils.get_working_tools()
|
| 30 |
-
working_resources = compact_utils.get_working_resources()
|
| 31 |
-
|
| 32 |
-
print(f" ✅ Working tools: {working_tools}")
|
| 33 |
-
print(f" ✅ Working resources: {working_resources}")
|
| 34 |
-
|
| 35 |
-
if not working_tools and not working_resources:
|
| 36 |
-
print(" ❌ No tools or resources found!")
|
| 37 |
-
return False
|
| 38 |
-
|
| 39 |
-
# 2. Check LLM provider
|
| 40 |
-
print("\n2. 🤖 Checking LLM Provider...")
|
| 41 |
-
try:
|
| 42 |
-
llm_client = LLMClient()
|
| 43 |
-
print(" ✅ LLM client initialized")
|
| 44 |
-
|
| 45 |
-
# Test simple chat
|
| 46 |
-
response = await llm_client.chat([{"role": "user", "content": "Hello"}])
|
| 47 |
-
print(f" ✅ LLM chat working: {len(response)} characters")
|
| 48 |
-
|
| 49 |
-
except Exception as e:
|
| 50 |
-
print(f" ❌ LLM provider error: {e}")
|
| 51 |
-
return False
|
| 52 |
-
|
| 53 |
-
# 3. Check prompt builder
|
| 54 |
-
print("\n3. 📝 Checking Prompt Builder...")
|
| 55 |
-
try:
|
| 56 |
-
prompt_builder = PromptBuilder()
|
| 57 |
-
prompt = prompt_builder.build_tool_decision_prompt("Show me challenges")
|
| 58 |
-
print(f" ✅ Prompt builder working: {len(prompt)} characters")
|
| 59 |
-
|
| 60 |
-
except Exception as e:
|
| 61 |
-
print(f" ❌ Prompt builder error: {e}")
|
| 62 |
-
return False
|
| 63 |
-
|
| 64 |
-
# 4. Check tool executor
|
| 65 |
-
print("\n4. ⚡ Checking Tool Executor...")
|
| 66 |
-
try:
|
| 67 |
-
tool_executor = ToolExecutor()
|
| 68 |
-
available_tools = tool_executor.get_available_tools()
|
| 69 |
-
available_resources = tool_executor.get_available_resources()
|
| 70 |
-
|
| 71 |
-
print(f" ✅ Available tools: {available_tools}")
|
| 72 |
-
print(f" ✅ Available resources: {available_resources}")
|
| 73 |
-
|
| 74 |
-
except Exception as e:
|
| 75 |
-
print(f" ❌ Tool executor error: {e}")
|
| 76 |
-
return False
|
| 77 |
-
|
| 78 |
-
# 5. Check MCP connection
|
| 79 |
-
print("\n5. 🔌 Checking MCP Connection...")
|
| 80 |
-
try:
|
| 81 |
-
server_config = load_server_config()
|
| 82 |
-
print(f" ✅ Server config loaded: {server_config}")
|
| 83 |
-
|
| 84 |
-
async with StreamableHttpMCPClient(server_config) as client:
|
| 85 |
-
init_data = await client.initialize()
|
| 86 |
-
print(f" ✅ MCP connection successful: {init_data}")
|
| 87 |
-
|
| 88 |
-
# Test tools/list
|
| 89 |
-
tools_data = await client.call("tools/list", {})
|
| 90 |
-
tools = tools_data.get("result", {}).get("tools", [])
|
| 91 |
-
print(f" ✅ Tools available: {len(tools)} tools")
|
| 92 |
-
|
| 93 |
-
except Exception as e:
|
| 94 |
-
print(f" ❌ MCP connection error: {e}")
|
| 95 |
-
return False
|
| 96 |
-
|
| 97 |
-
print("\n✅ All components working!")
|
| 98 |
-
return True
|
| 99 |
-
|
| 100 |
-
async def test_specific_issue():
|
| 101 |
-
"""Test a specific user query to identify the issue."""
|
| 102 |
-
|
| 103 |
-
print("\n🎯 Testing Specific Query")
|
| 104 |
-
print("=" * 50)
|
| 105 |
-
|
| 106 |
-
user_message = "Show me active challenges"
|
| 107 |
-
print(f"User message: {user_message}")
|
| 108 |
-
|
| 109 |
-
# Initialize services
|
| 110 |
-
llm_client = LLMClient()
|
| 111 |
-
prompt_builder = PromptBuilder()
|
| 112 |
-
tool_executor = ToolExecutor()
|
| 113 |
-
|
| 114 |
-
# Step 1: Tool decision
|
| 115 |
-
print("\nStep 1: Tool Decision")
|
| 116 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(user_message)
|
| 117 |
-
print(f"Prompt length: {len(decision_prompt)} characters")
|
| 118 |
-
|
| 119 |
-
try:
|
| 120 |
-
decision_json = await llm_client.decide_tool(decision_prompt)
|
| 121 |
-
print(f"Decision: {decision_json}")
|
| 122 |
-
|
| 123 |
-
tool = decision_json.get("tool")
|
| 124 |
-
resource = decision_json.get("resource")
|
| 125 |
-
params = decision_json.get("params", {})
|
| 126 |
-
|
| 127 |
-
print(f"Tool: {tool}")
|
| 128 |
-
print(f"Resource: {resource}")
|
| 129 |
-
print(f"Params: {params}")
|
| 130 |
-
|
| 131 |
-
except Exception as e:
|
| 132 |
-
print(f"❌ Tool decision failed: {e}")
|
| 133 |
-
return
|
| 134 |
-
|
| 135 |
-
# Step 2: Tool execution
|
| 136 |
-
if tool or resource:
|
| 137 |
-
print("\nStep 2: Tool Execution")
|
| 138 |
-
|
| 139 |
-
available_tools = tool_executor.get_available_tools()
|
| 140 |
-
available_resources = tool_executor.get_available_resources()
|
| 141 |
-
|
| 142 |
-
if tool and tool in available_tools:
|
| 143 |
-
target_name = tool
|
| 144 |
-
target_type = "tool"
|
| 145 |
-
elif resource and resource in available_resources:
|
| 146 |
-
target_name = resource
|
| 147 |
-
target_type = "resource"
|
| 148 |
-
else:
|
| 149 |
-
print(f"❌ Invalid target: {tool or resource}")
|
| 150 |
-
return
|
| 151 |
-
|
| 152 |
-
print(f"Executing: {target_name} ({target_type})")
|
| 153 |
-
|
| 154 |
-
from src.domain.models.tool import ToolRequest
|
| 155 |
-
request = ToolRequest(tool=target_name, params=params)
|
| 156 |
-
|
| 157 |
-
try:
|
| 158 |
-
result = await tool_executor.execute(request)
|
| 159 |
-
print(f"Result status: {result.status}")
|
| 160 |
-
|
| 161 |
-
if result.status == "success":
|
| 162 |
-
print("✅ Tool execution successful!")
|
| 163 |
-
if result.data:
|
| 164 |
-
print(f"Data length: {len(str(result.data))} characters")
|
| 165 |
-
else:
|
| 166 |
-
print(f"❌ Tool execution failed: {result.message}")
|
| 167 |
-
|
| 168 |
-
except Exception as e:
|
| 169 |
-
print(f"❌ Exception during execution: {e}")
|
| 170 |
-
import traceback
|
| 171 |
-
traceback.print_exc()
|
| 172 |
-
|
| 173 |
-
async def main():
|
| 174 |
-
"""Run diagnostics."""
|
| 175 |
-
try:
|
| 176 |
-
# Run component diagnostics
|
| 177 |
-
if await diagnose_components():
|
| 178 |
-
# If all components are working, test specific issue
|
| 179 |
-
await test_specific_issue()
|
| 180 |
-
else:
|
| 181 |
-
print("\n❌ Component diagnostics failed. Please check the issues above.")
|
| 182 |
-
|
| 183 |
-
except Exception as e:
|
| 184 |
-
print(f"\n❌ Diagnostic failed: {e}")
|
| 185 |
-
import traceback
|
| 186 |
-
traceback.print_exc()
|
| 187 |
-
|
| 188 |
-
if __name__ == "__main__":
|
| 189 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/migrate_to_v2.py
DELETED
|
@@ -1,269 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Migration script from v1.x to v2.0 architecture.
|
| 4 |
-
Helps preserve existing data and configuration while upgrading to new structure.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import os
|
| 8 |
-
import json
|
| 9 |
-
import shutil
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from typing import Dict, Any, List
|
| 12 |
-
import argparse
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class MigrationTool:
|
| 16 |
-
"""Tool to migrate from v1.x to v2.0 architecture."""
|
| 17 |
-
|
| 18 |
-
def __init__(self, old_app_dir: str = "app", backup_dir: str = "backup_v1"):
|
| 19 |
-
self.old_app_dir = Path(old_app_dir)
|
| 20 |
-
self.backup_dir = Path(backup_dir)
|
| 21 |
-
self.src_dir = Path("src")
|
| 22 |
-
|
| 23 |
-
def create_backup(self) -> None:
|
| 24 |
-
"""Create backup of old structure."""
|
| 25 |
-
if self.old_app_dir.exists():
|
| 26 |
-
print(f"📦 Creating backup in {self.backup_dir}/")
|
| 27 |
-
if self.backup_dir.exists():
|
| 28 |
-
shutil.rmtree(self.backup_dir)
|
| 29 |
-
shutil.copytree(self.old_app_dir, self.backup_dir)
|
| 30 |
-
print("✅ Backup created successfully")
|
| 31 |
-
|
| 32 |
-
def migrate_config(self) -> None:
|
| 33 |
-
"""Migrate configuration files."""
|
| 34 |
-
print("🔧 Migrating configuration...")
|
| 35 |
-
|
| 36 |
-
# Migrate .env file format
|
| 37 |
-
env_file = Path(".env")
|
| 38 |
-
if env_file.exists():
|
| 39 |
-
self._migrate_env_file(env_file)
|
| 40 |
-
|
| 41 |
-
# Migrate mcp-config.json if it exists
|
| 42 |
-
mcp_config = Path("mcp-config.json")
|
| 43 |
-
if mcp_config.exists():
|
| 44 |
-
self._migrate_mcp_config(mcp_config)
|
| 45 |
-
|
| 46 |
-
print("✅ Configuration migrated")
|
| 47 |
-
|
| 48 |
-
def _migrate_env_file(self, env_file: Path) -> None:
|
| 49 |
-
"""Migrate .env file to new format."""
|
| 50 |
-
old_env = env_file.read_text()
|
| 51 |
-
new_env_lines = []
|
| 52 |
-
|
| 53 |
-
mapping = {
|
| 54 |
-
"HF_TOKEN": "LLM_API_KEY",
|
| 55 |
-
"HF_MODEL": "LLM_MODEL",
|
| 56 |
-
"LLM_API_URL": "LLM_API_URL",
|
| 57 |
-
"LLM_TEMPERATURE": "LLM_TEMPERATURE",
|
| 58 |
-
"MCP_SERVER_URL": "MCP_SERVER_URL",
|
| 59 |
-
"MCP_TOKEN": "MCP_API_KEY",
|
| 60 |
-
"EXCLUDE_AUTH_REQUIRED_ENDPOINTS": "EXCLUDE_AUTH_ENDPOINTS",
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
for line in old_env.split('\n'):
|
| 64 |
-
if '=' in line and not line.strip().startswith('#'):
|
| 65 |
-
key, value = line.split('=', 1)
|
| 66 |
-
key = key.strip()
|
| 67 |
-
if key in mapping:
|
| 68 |
-
new_key = mapping[key]
|
| 69 |
-
new_env_lines.append(f"{new_key}={value}")
|
| 70 |
-
else:
|
| 71 |
-
new_env_lines.append(line)
|
| 72 |
-
else:
|
| 73 |
-
new_env_lines.append(line)
|
| 74 |
-
|
| 75 |
-
# Add new default values
|
| 76 |
-
new_defaults = [
|
| 77 |
-
"",
|
| 78 |
-
"# New v2.0 settings",
|
| 79 |
-
"MCP_PROTOCOL_VERSION=2024-11-05",
|
| 80 |
-
"MCP_CLIENT_NAME=topcoder-mcp-agent",
|
| 81 |
-
"MCP_CLIENT_VERSION=2.0.0",
|
| 82 |
-
"LOG_LEVEL=INFO",
|
| 83 |
-
"DEBUG=false",
|
| 84 |
-
"ENVIRONMENT=development"
|
| 85 |
-
]
|
| 86 |
-
|
| 87 |
-
new_env_content = '\n'.join(new_env_lines + new_defaults)
|
| 88 |
-
env_file.write_text(new_env_content)
|
| 89 |
-
print(f" 📝 Updated .env file with new format")
|
| 90 |
-
|
| 91 |
-
def _migrate_mcp_config(self, mcp_config: Path) -> None:
|
| 92 |
-
"""Archive old MCP config file."""
|
| 93 |
-
backup_config = self.backup_dir / "mcp-config.json"
|
| 94 |
-
shutil.copy2(mcp_config, backup_config)
|
| 95 |
-
print(f" 📋 Backed up mcp-config.json")
|
| 96 |
-
|
| 97 |
-
def migrate_data(self) -> None:
|
| 98 |
-
"""Migrate data files."""
|
| 99 |
-
print("📊 Migrating data files...")
|
| 100 |
-
|
| 101 |
-
# Create new data structure
|
| 102 |
-
data_dir = Path("data")
|
| 103 |
-
data_dir.mkdir(exist_ok=True)
|
| 104 |
-
(data_dir / "cache").mkdir(exist_ok=True)
|
| 105 |
-
(data_dir / "catalog").mkdir(exist_ok=True)
|
| 106 |
-
|
| 107 |
-
# Migrate mcp_catalog if it exists
|
| 108 |
-
old_catalog = Path("mcp_catalog")
|
| 109 |
-
if old_catalog.exists():
|
| 110 |
-
new_catalog = data_dir / "catalog"
|
| 111 |
-
if new_catalog.exists():
|
| 112 |
-
shutil.rmtree(new_catalog)
|
| 113 |
-
shutil.copytree(old_catalog, new_catalog)
|
| 114 |
-
print(f" 📁 Migrated mcp_catalog to {new_catalog}")
|
| 115 |
-
|
| 116 |
-
# Migrate any compact specs
|
| 117 |
-
old_compact_specs = self.old_app_dir / "config" / "compact_api_specs.py"
|
| 118 |
-
if old_compact_specs.exists():
|
| 119 |
-
new_specs_dir = data_dir / "specs"
|
| 120 |
-
new_specs_dir.mkdir(exist_ok=True)
|
| 121 |
-
|
| 122 |
-
# Convert Python file to JSON
|
| 123 |
-
self._convert_compact_specs_to_json(old_compact_specs, new_specs_dir / "compact_specs.json")
|
| 124 |
-
|
| 125 |
-
print("✅ Data migration completed")
|
| 126 |
-
|
| 127 |
-
def _convert_compact_specs_to_json(self, old_file: Path, new_file: Path) -> None:
|
| 128 |
-
"""Convert old Python compact specs to JSON format."""
|
| 129 |
-
try:
|
| 130 |
-
# Read the old file and extract COMPACT_API_SPECS
|
| 131 |
-
old_content = old_file.read_text()
|
| 132 |
-
|
| 133 |
-
# This is a simplified conversion - in practice, you might want to
|
| 134 |
-
# import and execute the Python file to get the actual dictionary
|
| 135 |
-
if "COMPACT_API_SPECS = {" in old_content:
|
| 136 |
-
# Extract the dictionary part (this is a simplified approach)
|
| 137 |
-
start = old_content.find("COMPACT_API_SPECS = {")
|
| 138 |
-
if start != -1:
|
| 139 |
-
# For now, just copy the file for manual migration
|
| 140 |
-
shutil.copy2(old_file, new_file.with_suffix('.py.bak'))
|
| 141 |
-
print(f" 📄 Backed up compact specs to {new_file.with_suffix('.py.bak')}")
|
| 142 |
-
print(f" ⚠️ Manual migration needed for compact specs format")
|
| 143 |
-
except Exception as e:
|
| 144 |
-
print(f" ⚠️ Could not convert compact specs: {e}")
|
| 145 |
-
|
| 146 |
-
def remove_old_structure(self) -> None:
|
| 147 |
-
"""Remove old app directory structure."""
|
| 148 |
-
print("🗑️ Removing old structure...")
|
| 149 |
-
|
| 150 |
-
if self.old_app_dir.exists():
|
| 151 |
-
# Double-check backup exists
|
| 152 |
-
if not self.backup_dir.exists():
|
| 153 |
-
print("❌ Backup not found! Skipping removal for safety.")
|
| 154 |
-
return
|
| 155 |
-
|
| 156 |
-
shutil.rmtree(self.old_app_dir)
|
| 157 |
-
print(f"✅ Removed old {self.old_app_dir} directory")
|
| 158 |
-
|
| 159 |
-
# Remove old files
|
| 160 |
-
old_files = ["setup.py"]
|
| 161 |
-
for file_name in old_files:
|
| 162 |
-
file_path = Path(file_name)
|
| 163 |
-
if file_path.exists():
|
| 164 |
-
file_path.unlink()
|
| 165 |
-
print(f" 🗑️ Removed {file_name}")
|
| 166 |
-
|
| 167 |
-
def verify_migration(self) -> bool:
|
| 168 |
-
"""Verify migration was successful."""
|
| 169 |
-
print("🔍 Verifying migration...")
|
| 170 |
-
|
| 171 |
-
required_dirs = [
|
| 172 |
-
"src",
|
| 173 |
-
"src/presentation",
|
| 174 |
-
"src/application",
|
| 175 |
-
"src/domain",
|
| 176 |
-
"src/mcp",
|
| 177 |
-
"src/infrastructure"
|
| 178 |
-
]
|
| 179 |
-
|
| 180 |
-
required_files = [
|
| 181 |
-
"pyproject.toml",
|
| 182 |
-
"README.md",
|
| 183 |
-
"src/main.py"
|
| 184 |
-
]
|
| 185 |
-
|
| 186 |
-
missing = []
|
| 187 |
-
|
| 188 |
-
for dir_path in required_dirs:
|
| 189 |
-
if not Path(dir_path).exists():
|
| 190 |
-
missing.append(dir_path)
|
| 191 |
-
|
| 192 |
-
for file_path in required_files:
|
| 193 |
-
if not Path(file_path).exists():
|
| 194 |
-
missing.append(file_path)
|
| 195 |
-
|
| 196 |
-
if missing:
|
| 197 |
-
print(f"❌ Missing required items: {missing}")
|
| 198 |
-
return False
|
| 199 |
-
|
| 200 |
-
print("✅ Migration verification passed")
|
| 201 |
-
return True
|
| 202 |
-
|
| 203 |
-
def run_migration(self, skip_backup: bool = False, skip_removal: bool = False) -> None:
|
| 204 |
-
"""Run the complete migration process."""
|
| 205 |
-
print("🚀 Starting migration from v1.x to v2.0...")
|
| 206 |
-
print()
|
| 207 |
-
|
| 208 |
-
try:
|
| 209 |
-
if not skip_backup:
|
| 210 |
-
self.create_backup()
|
| 211 |
-
print()
|
| 212 |
-
|
| 213 |
-
self.migrate_config()
|
| 214 |
-
print()
|
| 215 |
-
|
| 216 |
-
self.migrate_data()
|
| 217 |
-
print()
|
| 218 |
-
|
| 219 |
-
if self.verify_migration():
|
| 220 |
-
print()
|
| 221 |
-
if not skip_removal:
|
| 222 |
-
self.remove_old_structure()
|
| 223 |
-
print()
|
| 224 |
-
|
| 225 |
-
print("🎉 Migration completed successfully!")
|
| 226 |
-
print()
|
| 227 |
-
print("Next steps:")
|
| 228 |
-
print("1. Review your .env file and update any API keys")
|
| 229 |
-
print("2. Install new dependencies: pip install -e .")
|
| 230 |
-
print("3. Run the application: python src/main.py")
|
| 231 |
-
print("4. Check the backup_v1/ directory if you need any old files")
|
| 232 |
-
else:
|
| 233 |
-
print("❌ Migration verification failed")
|
| 234 |
-
|
| 235 |
-
except Exception as e:
|
| 236 |
-
print(f"💥 Migration failed: {e}")
|
| 237 |
-
print("Your original files are safely backed up in backup_v1/")
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
def main():
|
| 241 |
-
"""Main CLI entry point."""
|
| 242 |
-
parser = argparse.ArgumentParser(description="Migrate Topcoder MCP Agent to v2.0")
|
| 243 |
-
parser.add_argument(
|
| 244 |
-
"--skip-backup",
|
| 245 |
-
action="store_true",
|
| 246 |
-
help="Skip creating backup of old structure"
|
| 247 |
-
)
|
| 248 |
-
parser.add_argument(
|
| 249 |
-
"--skip-removal",
|
| 250 |
-
action="store_true",
|
| 251 |
-
help="Skip removing old structure after migration"
|
| 252 |
-
)
|
| 253 |
-
parser.add_argument(
|
| 254 |
-
"--old-app-dir",
|
| 255 |
-
default="app",
|
| 256 |
-
help="Path to old app directory (default: app)"
|
| 257 |
-
)
|
| 258 |
-
|
| 259 |
-
args = parser.parse_args()
|
| 260 |
-
|
| 261 |
-
migrator = MigrationTool(old_app_dir=args.old_app_dir)
|
| 262 |
-
migrator.run_migration(
|
| 263 |
-
skip_backup=args.skip_backup,
|
| 264 |
-
skip_removal=args.skip_removal
|
| 265 |
-
)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
if __name__ == "__main__":
|
| 269 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_context_aware_selection.py
DELETED
|
@@ -1,207 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test script for context-aware tool selection with conversation history.
|
| 4 |
-
This script demonstrates how the enhanced tool selection works with conversation context.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import asyncio
|
| 8 |
-
import sys
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
# Add src to path so we can import modules
|
| 12 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 13 |
-
|
| 14 |
-
# Add root directory to path so we can import config module
|
| 15 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
-
|
| 17 |
-
from src.infrastructure.providers.llm_provider import LLMClient
|
| 18 |
-
from src.application.services.prompt_service import PromptBuilder
|
| 19 |
-
from src.domain.services.conversation_history import ConversationHistoryService
|
| 20 |
-
from config import settings
|
| 21 |
-
|
| 22 |
-
async def test_context_aware_selection():
|
| 23 |
-
"""Test context-aware tool selection with conversation history."""
|
| 24 |
-
|
| 25 |
-
print("🧠 Testing Context-Aware Tool Selection")
|
| 26 |
-
print("=" * 50)
|
| 27 |
-
|
| 28 |
-
# Initialize services
|
| 29 |
-
llm_client = LLMClient()
|
| 30 |
-
prompt_builder = PromptBuilder()
|
| 31 |
-
history_service = ConversationHistoryService()
|
| 32 |
-
session_id = "context_test"
|
| 33 |
-
|
| 34 |
-
# Test conversation scenarios
|
| 35 |
-
test_scenarios = [
|
| 36 |
-
{
|
| 37 |
-
"name": "Challenge Follow-up with AI Filter",
|
| 38 |
-
"conversation": [
|
| 39 |
-
("user", "Show me active challenges"),
|
| 40 |
-
("assistant", "I found several active challenges. Here are some highlights: AI Challenge #123, Web Development #456, Mobile App #789."),
|
| 41 |
-
("user", "What about AI challenges specifically?")
|
| 42 |
-
],
|
| 43 |
-
"expected_behavior": "Should select query-tc-challenges with track=AI filter"
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
"name": "Skills Follow-up with Programming Filter",
|
| 47 |
-
"conversation": [
|
| 48 |
-
("user", "What programming skills are available?"),
|
| 49 |
-
("assistant", "I found several programming skills: Python, JavaScript, Java, React, Angular, Node.js."),
|
| 50 |
-
("user", "Show me more programming skills")
|
| 51 |
-
],
|
| 52 |
-
"expected_behavior": "Should select query-tc-skills with name filter for programming"
|
| 53 |
-
},
|
| 54 |
-
{
|
| 55 |
-
"name": "Challenge Status Refinement",
|
| 56 |
-
"conversation": [
|
| 57 |
-
("user", "Show me active challenges"),
|
| 58 |
-
("assistant", "I found 15 active challenges. Here are some highlights: AI Challenge #123, Web Development #456."),
|
| 59 |
-
("user", "What about completed challenges?")
|
| 60 |
-
],
|
| 61 |
-
"expected_behavior": "Should select query-tc-challenges with status=Completed"
|
| 62 |
-
},
|
| 63 |
-
{
|
| 64 |
-
"name": "Skills Technology Refinement",
|
| 65 |
-
"conversation": [
|
| 66 |
-
("user", "What skills are available?"),
|
| 67 |
-
("assistant", "I found various skills: Python, JavaScript, Machine Learning, AI, React, Angular."),
|
| 68 |
-
("user", "Tell me about AI and Machine Learning skills")
|
| 69 |
-
],
|
| 70 |
-
"expected_behavior": "Should select query-tc-skills with name filter for AI/ML"
|
| 71 |
-
}
|
| 72 |
-
]
|
| 73 |
-
|
| 74 |
-
for i, scenario in enumerate(test_scenarios):
|
| 75 |
-
print(f"\n📝 Test {i+1}: {scenario['name']}")
|
| 76 |
-
print(f"Expected behavior: {scenario['expected_behavior']}")
|
| 77 |
-
|
| 78 |
-
# Clear history for this test
|
| 79 |
-
history_service.clear_history(session_id)
|
| 80 |
-
|
| 81 |
-
# Simulate conversation
|
| 82 |
-
print("\n💬 Simulating conversation...")
|
| 83 |
-
for j, (role, content) in enumerate(scenario["conversation"]):
|
| 84 |
-
print(f" {role.upper()}: {content}")
|
| 85 |
-
|
| 86 |
-
if role == "user":
|
| 87 |
-
# Add user message to history
|
| 88 |
-
history_service.add_message(session_id, role, content)
|
| 89 |
-
|
| 90 |
-
# Get conversation history
|
| 91 |
-
conversation_history = history_service.get_conversation_history(session_id)
|
| 92 |
-
print(f" 📚 History length: {len(conversation_history)} messages")
|
| 93 |
-
|
| 94 |
-
# Test tool decision with enhanced context
|
| 95 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(content, conversation_history)
|
| 96 |
-
decision_json = await llm_client.decide_tool(decision_prompt, conversation_history)
|
| 97 |
-
|
| 98 |
-
tool = decision_json.get("tool")
|
| 99 |
-
resource = decision_json.get("resource")
|
| 100 |
-
params = decision_json.get("params", {})
|
| 101 |
-
|
| 102 |
-
print(f" 🔍 Tool decision: tool={tool}, resource={resource}")
|
| 103 |
-
print(f" 📊 Parameters: {params}")
|
| 104 |
-
|
| 105 |
-
# Add assistant response to history
|
| 106 |
-
history_service.add_message(session_id, "assistant", content)
|
| 107 |
-
|
| 108 |
-
print(f" ✅ Test {i+1} completed")
|
| 109 |
-
|
| 110 |
-
print("\n✅ All context-aware selection tests completed!")
|
| 111 |
-
|
| 112 |
-
async def test_parameter_extraction_with_context():
|
| 113 |
-
"""Test parameter extraction with conversation context."""
|
| 114 |
-
|
| 115 |
-
print("\n🔧 Testing Parameter Extraction with Context")
|
| 116 |
-
print("=" * 50)
|
| 117 |
-
|
| 118 |
-
# Initialize services
|
| 119 |
-
llm_client = LLMClient()
|
| 120 |
-
history_service = ConversationHistoryService()
|
| 121 |
-
session_id = "param_test"
|
| 122 |
-
|
| 123 |
-
# Test parameter extraction scenarios
|
| 124 |
-
test_cases = [
|
| 125 |
-
{
|
| 126 |
-
"name": "AI Challenge Follow-up",
|
| 127 |
-
"conversation": [
|
| 128 |
-
("user", "Show me active challenges"),
|
| 129 |
-
("assistant", "I found several active challenges: AI Challenge #123, Web Development #456."),
|
| 130 |
-
("user", "What about AI challenges specifically?")
|
| 131 |
-
],
|
| 132 |
-
"tool": "query-tc-challenges",
|
| 133 |
-
"expected_params": ["track", "status"]
|
| 134 |
-
},
|
| 135 |
-
{
|
| 136 |
-
"name": "Programming Skills Follow-up",
|
| 137 |
-
"conversation": [
|
| 138 |
-
("user", "What programming skills are available?"),
|
| 139 |
-
("assistant", "I found programming skills: Python, JavaScript, Java."),
|
| 140 |
-
("user", "Show me more programming skills")
|
| 141 |
-
],
|
| 142 |
-
"tool": "query-tc-skills",
|
| 143 |
-
"expected_params": ["name"]
|
| 144 |
-
}
|
| 145 |
-
]
|
| 146 |
-
|
| 147 |
-
for i, test_case in enumerate(test_cases):
|
| 148 |
-
print(f"\n📝 Test {i+1}: {test_case['name']}")
|
| 149 |
-
|
| 150 |
-
# Clear history for this test
|
| 151 |
-
history_service.clear_history(session_id)
|
| 152 |
-
|
| 153 |
-
# Simulate conversation
|
| 154 |
-
for role, content in test_case["conversation"]:
|
| 155 |
-
history_service.add_message(session_id, role, content)
|
| 156 |
-
|
| 157 |
-
# Get conversation history
|
| 158 |
-
conversation_history = history_service.get_conversation_history(session_id)
|
| 159 |
-
|
| 160 |
-
# Test parameter extraction with context
|
| 161 |
-
from config.prompt_templates import PromptTemplates
|
| 162 |
-
|
| 163 |
-
# Create context summary
|
| 164 |
-
context_messages = conversation_history[-4:]
|
| 165 |
-
context_text = "\n".join([
|
| 166 |
-
f"{msg['role'].upper()}: {msg['content'][:100]}{'...' if len(msg['content']) > 100 else ''}"
|
| 167 |
-
for msg in context_messages
|
| 168 |
-
])
|
| 169 |
-
|
| 170 |
-
param_extraction_prompt = PromptTemplates.get_tool_param_extraction_prompt(
|
| 171 |
-
tool_name=test_case["tool"],
|
| 172 |
-
user_message=test_case["conversation"][-1][1], # Last user message
|
| 173 |
-
tool_parameters="Available parameters for the tool",
|
| 174 |
-
conversation_context=context_text
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
param_response = await llm_client.complete_json(param_extraction_prompt, conversation_history)
|
| 178 |
-
params = param_response.get("params", {})
|
| 179 |
-
|
| 180 |
-
print(f" 🔍 Extracted parameters: {params}")
|
| 181 |
-
print(f" 📊 Expected parameters: {test_case['expected_params']}")
|
| 182 |
-
|
| 183 |
-
# Check if expected parameters are present
|
| 184 |
-
extracted_param_names = list(params.keys())
|
| 185 |
-
expected_params = test_case["expected_params"]
|
| 186 |
-
|
| 187 |
-
matches = [param for param in expected_params if param in extracted_param_names]
|
| 188 |
-
if matches:
|
| 189 |
-
print(f" ✅ Found expected parameters: {matches}")
|
| 190 |
-
else:
|
| 191 |
-
print(f" ⚠️ Expected parameters not found: {expected_params}")
|
| 192 |
-
|
| 193 |
-
print("\n✅ All parameter extraction tests completed!")
|
| 194 |
-
|
| 195 |
-
async def main():
|
| 196 |
-
"""Run all context-aware tests."""
|
| 197 |
-
try:
|
| 198 |
-
await test_context_aware_selection()
|
| 199 |
-
await test_parameter_extraction_with_context()
|
| 200 |
-
print("\n🎉 All context-aware tests completed successfully!")
|
| 201 |
-
except Exception as e:
|
| 202 |
-
print(f"\n❌ Test failed: {e}")
|
| 203 |
-
import traceback
|
| 204 |
-
traceback.print_exc()
|
| 205 |
-
|
| 206 |
-
if __name__ == "__main__":
|
| 207 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_full_execution.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Comprehensive test for full tool execution flow.
|
| 4 |
-
This script tests the complete pipeline from user input to tool execution.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import asyncio
|
| 8 |
-
import sys
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
# Add src to path so we can import modules
|
| 12 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 13 |
-
|
| 14 |
-
# Add root directory to path so we can import config module
|
| 15 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
-
|
| 17 |
-
from src.infrastructure.providers.llm_provider import LLMClient
|
| 18 |
-
from src.application.services.prompt_service import PromptBuilder
|
| 19 |
-
from src.application.services.tool_service import ToolExecutor
|
| 20 |
-
from src.domain.services.conversation_history import ConversationHistoryService
|
| 21 |
-
from src.domain.models.tool import ToolRequest
|
| 22 |
-
from config import settings
|
| 23 |
-
|
| 24 |
-
async def test_full_execution():
|
| 25 |
-
"""Test the complete execution flow."""
|
| 26 |
-
|
| 27 |
-
print("🧪 Testing Full Execution Flow")
|
| 28 |
-
print("=" * 50)
|
| 29 |
-
|
| 30 |
-
# Initialize services
|
| 31 |
-
llm_client = LLMClient()
|
| 32 |
-
prompt_builder = PromptBuilder()
|
| 33 |
-
tool_executor = ToolExecutor()
|
| 34 |
-
history_service = ConversationHistoryService()
|
| 35 |
-
|
| 36 |
-
# Test cases
|
| 37 |
-
test_cases = [
|
| 38 |
-
{
|
| 39 |
-
"name": "Challenge query",
|
| 40 |
-
"message": "Show me active challenges",
|
| 41 |
-
"expected_tool": "query-tc-challenges"
|
| 42 |
-
},
|
| 43 |
-
{
|
| 44 |
-
"name": "Skills query",
|
| 45 |
-
"message": "What programming skills are available?",
|
| 46 |
-
"expected_tool": "query-tc-skills"
|
| 47 |
-
}
|
| 48 |
-
]
|
| 49 |
-
|
| 50 |
-
for i, test_case in enumerate(test_cases):
|
| 51 |
-
print(f"\n📝 Test {i+1}: {test_case['name']}")
|
| 52 |
-
print(f" Message: {test_case['message']}")
|
| 53 |
-
|
| 54 |
-
# STEP 1: Tool decision
|
| 55 |
-
print(" 🔍 Step 1: Tool decision...")
|
| 56 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(test_case['message'])
|
| 57 |
-
decision_json = await llm_client.decide_tool(decision_prompt)
|
| 58 |
-
|
| 59 |
-
tool = decision_json.get("tool")
|
| 60 |
-
resource = decision_json.get("resource")
|
| 61 |
-
params = decision_json.get("params", {})
|
| 62 |
-
|
| 63 |
-
print(f" 📊 Decision: tool={tool}, resource={resource}, params={params}")
|
| 64 |
-
|
| 65 |
-
# STEP 2: Determine target
|
| 66 |
-
available_tools = tool_executor.get_available_tools()
|
| 67 |
-
available_resources = tool_executor.get_available_resources()
|
| 68 |
-
|
| 69 |
-
if tool and tool in available_tools:
|
| 70 |
-
target_name = tool
|
| 71 |
-
target_type = "tool"
|
| 72 |
-
elif resource and resource in available_resources:
|
| 73 |
-
target_name = resource
|
| 74 |
-
target_type = "resource"
|
| 75 |
-
else:
|
| 76 |
-
print(f" ❌ Invalid selection: {tool or resource}")
|
| 77 |
-
continue
|
| 78 |
-
|
| 79 |
-
print(f" 🎯 Target: {target_name} ({target_type})")
|
| 80 |
-
|
| 81 |
-
# STEP 3: Execute tool
|
| 82 |
-
print(" ⚡ Step 3: Executing tool...")
|
| 83 |
-
request = ToolRequest(tool=target_name, params=params)
|
| 84 |
-
|
| 85 |
-
try:
|
| 86 |
-
result = await tool_executor.execute(request)
|
| 87 |
-
print(f" 📊 Result status: {result.status}")
|
| 88 |
-
|
| 89 |
-
if result.status == "success":
|
| 90 |
-
print(" ✅ Tool execution successful!")
|
| 91 |
-
if result.data:
|
| 92 |
-
print(f" 📄 Data length: {len(str(result.data))} characters")
|
| 93 |
-
else:
|
| 94 |
-
print(f" ❌ Tool execution failed: {result.message}")
|
| 95 |
-
|
| 96 |
-
except Exception as e:
|
| 97 |
-
print(f" ❌ Exception during execution: {e}")
|
| 98 |
-
|
| 99 |
-
print("\n✅ Full execution tests completed!")
|
| 100 |
-
|
| 101 |
-
async def test_conversation_with_execution():
|
| 102 |
-
"""Test conversation flow with actual tool execution."""
|
| 103 |
-
|
| 104 |
-
print("\n💬 Testing Conversation with Execution")
|
| 105 |
-
print("=" * 50)
|
| 106 |
-
|
| 107 |
-
# Initialize services
|
| 108 |
-
llm_client = LLMClient()
|
| 109 |
-
prompt_builder = PromptBuilder()
|
| 110 |
-
tool_executor = ToolExecutor()
|
| 111 |
-
history_service = ConversationHistoryService()
|
| 112 |
-
session_id = "execution_test"
|
| 113 |
-
|
| 114 |
-
# Simulate a conversation with tool execution
|
| 115 |
-
conversation = [
|
| 116 |
-
("user", "Show me active challenges"),
|
| 117 |
-
("user", "What about AI challenges specifically?")
|
| 118 |
-
]
|
| 119 |
-
|
| 120 |
-
print("📝 Simulating conversation with execution...")
|
| 121 |
-
|
| 122 |
-
for i, (role, content) in enumerate(conversation):
|
| 123 |
-
print(f"\n {role.upper()}: {content}")
|
| 124 |
-
|
| 125 |
-
# Add to history
|
| 126 |
-
history_service.add_message(session_id, role, content)
|
| 127 |
-
conversation_history = history_service.get_conversation_history(session_id)
|
| 128 |
-
|
| 129 |
-
# Tool decision with history
|
| 130 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(content, conversation_history)
|
| 131 |
-
decision_json = await llm_client.decide_tool(decision_prompt, conversation_history)
|
| 132 |
-
|
| 133 |
-
tool = decision_json.get("tool")
|
| 134 |
-
resource = decision_json.get("resource")
|
| 135 |
-
params = decision_json.get("params", {})
|
| 136 |
-
|
| 137 |
-
print(f" 🔍 Decision: tool={tool}, resource={resource}, params={params}")
|
| 138 |
-
|
| 139 |
-
# Execute if valid
|
| 140 |
-
if tool or resource:
|
| 141 |
-
available_tools = tool_executor.get_available_tools()
|
| 142 |
-
available_resources = tool_executor.get_available_resources()
|
| 143 |
-
|
| 144 |
-
if tool and tool in available_tools:
|
| 145 |
-
target_name = tool
|
| 146 |
-
elif resource and resource in available_resources:
|
| 147 |
-
target_name = resource
|
| 148 |
-
else:
|
| 149 |
-
print(f" ❌ Invalid target: {tool or resource}")
|
| 150 |
-
continue
|
| 151 |
-
|
| 152 |
-
print(f" ⚡ Executing: {target_name}")
|
| 153 |
-
request = ToolRequest(tool=target_name, params=params)
|
| 154 |
-
|
| 155 |
-
try:
|
| 156 |
-
result = await tool_executor.execute(request)
|
| 157 |
-
print(f" 📊 Result: {result.status}")
|
| 158 |
-
|
| 159 |
-
if result.status == "success":
|
| 160 |
-
print(" ✅ Execution successful!")
|
| 161 |
-
else:
|
| 162 |
-
print(f" ❌ Execution failed: {result.message}")
|
| 163 |
-
|
| 164 |
-
except Exception as e:
|
| 165 |
-
print(f" ❌ Exception: {e}")
|
| 166 |
-
|
| 167 |
-
# Add assistant response to history
|
| 168 |
-
history_service.add_message(session_id, "assistant", f"Response to: {content}")
|
| 169 |
-
|
| 170 |
-
print("\n✅ Conversation with execution test completed!")
|
| 171 |
-
|
| 172 |
-
async def main():
|
| 173 |
-
"""Run all tests."""
|
| 174 |
-
try:
|
| 175 |
-
await test_full_execution()
|
| 176 |
-
await test_conversation_with_execution()
|
| 177 |
-
print("\n🎉 All execution tests completed successfully!")
|
| 178 |
-
except Exception as e:
|
| 179 |
-
print(f"\n❌ Test failed: {e}")
|
| 180 |
-
import traceback
|
| 181 |
-
traceback.print_exc()
|
| 182 |
-
|
| 183 |
-
if __name__ == "__main__":
|
| 184 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_tool_selection.py
DELETED
|
@@ -1,156 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test script for tool selection and tool calls with conversation history.
|
| 4 |
-
This script tests the core functionality to ensure everything works correctly.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import asyncio
|
| 8 |
-
import sys
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
# Add src to path so we can import modules
|
| 12 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 13 |
-
|
| 14 |
-
# Add root directory to path so we can import config module
|
| 15 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
-
|
| 17 |
-
from src.infrastructure.providers.llm_provider import LLMClient
|
| 18 |
-
from src.application.services.prompt_service import PromptBuilder
|
| 19 |
-
from src.domain.services.conversation_history import ConversationHistoryService
|
| 20 |
-
from config import settings
|
| 21 |
-
|
| 22 |
-
async def test_tool_selection():
|
| 23 |
-
"""Test tool selection with and without conversation history."""
|
| 24 |
-
|
| 25 |
-
print("🧪 Testing Tool Selection")
|
| 26 |
-
print("=" * 50)
|
| 27 |
-
|
| 28 |
-
# Initialize services
|
| 29 |
-
llm_client = LLMClient()
|
| 30 |
-
prompt_builder = PromptBuilder()
|
| 31 |
-
history_service = ConversationHistoryService()
|
| 32 |
-
|
| 33 |
-
# Test cases
|
| 34 |
-
test_cases = [
|
| 35 |
-
{
|
| 36 |
-
"name": "Simple challenge query",
|
| 37 |
-
"message": "Show me active challenges",
|
| 38 |
-
"expected_tool": "query-tc-challenges"
|
| 39 |
-
},
|
| 40 |
-
{
|
| 41 |
-
"name": "Skills query",
|
| 42 |
-
"message": "What programming skills are available?",
|
| 43 |
-
"expected_tool": "query-tc-skills"
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
"name": "General chat",
|
| 47 |
-
"message": "Hello, how are you?",
|
| 48 |
-
"expected_tool": "chat"
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"name": "Member query with handles array",
|
| 52 |
-
"message": "Show me member abhishek",
|
| 53 |
-
"expected_tool": "Member_V6_API_Swagger"
|
| 54 |
-
}
|
| 55 |
-
]
|
| 56 |
-
|
| 57 |
-
for i, test_case in enumerate(test_cases):
|
| 58 |
-
print(f"\n📝 Test {i+1}: {test_case['name']}")
|
| 59 |
-
print(f" Message: {test_case['message']}")
|
| 60 |
-
|
| 61 |
-
# Test without history
|
| 62 |
-
print(" 🔍 Testing without conversation history...")
|
| 63 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(test_case['message'])
|
| 64 |
-
decision_json = await llm_client.decide_tool(decision_prompt)
|
| 65 |
-
|
| 66 |
-
tool = decision_json.get("tool")
|
| 67 |
-
resource = decision_json.get("resource")
|
| 68 |
-
|
| 69 |
-
print(f" 📊 Result: tool={tool}, resource={resource}")
|
| 70 |
-
|
| 71 |
-
# Test with history
|
| 72 |
-
print(" 🔍 Testing with conversation history...")
|
| 73 |
-
session_id = "test_session"
|
| 74 |
-
|
| 75 |
-
# Add some context to history
|
| 76 |
-
history_service.add_message(session_id, "user", "I'm looking for challenges")
|
| 77 |
-
history_service.add_message(session_id, "assistant", "I can help you find challenges. What type are you interested in?")
|
| 78 |
-
|
| 79 |
-
conversation_history = history_service.get_conversation_history(session_id)
|
| 80 |
-
decision_prompt_with_history = prompt_builder.build_tool_decision_prompt(test_case['message'], conversation_history)
|
| 81 |
-
decision_json_with_history = await llm_client.decide_tool(decision_prompt_with_history, conversation_history)
|
| 82 |
-
|
| 83 |
-
tool_with_history = decision_json_with_history.get("tool")
|
| 84 |
-
resource_with_history = decision_json_with_history.get("resource")
|
| 85 |
-
|
| 86 |
-
print(f" 📊 Result with history: tool={tool_with_history}, resource={resource_with_history}")
|
| 87 |
-
|
| 88 |
-
# Check if results are reasonable
|
| 89 |
-
if tool or resource:
|
| 90 |
-
print(" ✅ Tool selection working")
|
| 91 |
-
else:
|
| 92 |
-
print(" ❌ No tool selected")
|
| 93 |
-
|
| 94 |
-
print("\n✅ Tool selection tests completed!")
|
| 95 |
-
|
| 96 |
-
async def test_conversation_flow():
|
| 97 |
-
"""Test a complete conversation flow."""
|
| 98 |
-
|
| 99 |
-
print("\n💬 Testing Conversation Flow")
|
| 100 |
-
print("=" * 50)
|
| 101 |
-
|
| 102 |
-
# Initialize services
|
| 103 |
-
llm_client = LLMClient()
|
| 104 |
-
history_service = ConversationHistoryService()
|
| 105 |
-
session_id = "conversation_test"
|
| 106 |
-
|
| 107 |
-
# Simulate a conversation
|
| 108 |
-
conversation = [
|
| 109 |
-
("user", "Show me active challenges"),
|
| 110 |
-
("assistant", "I found several active challenges. Here are some highlights: AI Challenge #123, Web Development #456."),
|
| 111 |
-
("user", "What about AI challenges specifically?"),
|
| 112 |
-
("assistant", "Looking at the active challenges, here are the AI-specific ones: AI Challenge #123 (Machine Learning), AI Challenge #124 (Computer Vision)."),
|
| 113 |
-
("user", "Tell me more about the Machine Learning challenge")
|
| 114 |
-
]
|
| 115 |
-
|
| 116 |
-
print("📝 Simulating conversation...")
|
| 117 |
-
|
| 118 |
-
for i, (role, content) in enumerate(conversation):
|
| 119 |
-
print(f" {role.upper()}: {content}")
|
| 120 |
-
|
| 121 |
-
if role == "user":
|
| 122 |
-
# Add user message to history
|
| 123 |
-
history_service.add_message(session_id, role, content)
|
| 124 |
-
|
| 125 |
-
# Get conversation history
|
| 126 |
-
conversation_history = history_service.get_conversation_history(session_id)
|
| 127 |
-
print(f" 📚 History length: {len(conversation_history)} messages")
|
| 128 |
-
|
| 129 |
-
# Test tool decision with history
|
| 130 |
-
from src.application.services.prompt_service import PromptBuilder
|
| 131 |
-
prompt_builder = PromptBuilder()
|
| 132 |
-
decision_prompt = prompt_builder.build_tool_decision_prompt(content, conversation_history)
|
| 133 |
-
decision_json = await llm_client.decide_tool(decision_prompt, conversation_history)
|
| 134 |
-
|
| 135 |
-
tool = decision_json.get("tool")
|
| 136 |
-
resource = decision_json.get("resource")
|
| 137 |
-
print(f" 🔍 Tool decision: tool={tool}, resource={resource}")
|
| 138 |
-
|
| 139 |
-
# Add assistant response to history
|
| 140 |
-
history_service.add_message(session_id, "assistant", content)
|
| 141 |
-
|
| 142 |
-
print("\n✅ Conversation flow test completed!")
|
| 143 |
-
|
| 144 |
-
async def main():
|
| 145 |
-
"""Run all tests."""
|
| 146 |
-
try:
|
| 147 |
-
await test_tool_selection()
|
| 148 |
-
await test_conversation_flow()
|
| 149 |
-
print("\n🎉 All tests completed successfully!")
|
| 150 |
-
except Exception as e:
|
| 151 |
-
print(f"\n❌ Test failed: {e}")
|
| 152 |
-
import traceback
|
| 153 |
-
traceback.print_exc()
|
| 154 |
-
|
| 155 |
-
if __name__ == "__main__":
|
| 156 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/application/services/specs_service.py
DELETED
|
@@ -1,486 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import asyncio
|
| 3 |
-
from typing import Dict, List, Any, Optional
|
| 4 |
-
from .catalog_utils import CatalogUtils
|
| 5 |
-
from .stream_client import load_server_config, StreamableHttpMCPClient
|
| 6 |
-
from config.settings import EXCLUDE_AUTH_REQUIRED_ENDPOINTS, EXCLUDE_401_RESPONSES, STRICT_AUTH_FILTERING
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class SpecsGenerator:
|
| 10 |
-
"""Generates compact API specs from cataloged data and tests functionality."""
|
| 11 |
-
|
| 12 |
-
def __init__(self, catalog_dir: str = "data/catalog"):
|
| 13 |
-
self.catalog_utils = CatalogUtils(catalog_dir)
|
| 14 |
-
self.compact_specs = {}
|
| 15 |
-
|
| 16 |
-
async def test_tool_functionality(self, tool_name: str) -> bool:
|
| 17 |
-
"""Test if a tool is working by making a minimal call."""
|
| 18 |
-
try:
|
| 19 |
-
server_config = load_server_config()
|
| 20 |
-
async with StreamableHttpMCPClient(server_config) as client:
|
| 21 |
-
await client.initialize()
|
| 22 |
-
|
| 23 |
-
# Get tool schema to find a valid test parameter
|
| 24 |
-
schema = self.catalog_utils.get_tool_schema(tool_name)
|
| 25 |
-
if not schema:
|
| 26 |
-
return False
|
| 27 |
-
|
| 28 |
-
input_schema = schema.get("inputSchema", {})
|
| 29 |
-
properties = input_schema.get("properties", {})
|
| 30 |
-
|
| 31 |
-
# Find a simple parameter to test with
|
| 32 |
-
test_params = {}
|
| 33 |
-
for param_name, param_info in properties.items():
|
| 34 |
-
if param_info.get("type") == "string" and not param_info.get("required", False):
|
| 35 |
-
test_params[param_name] = "test"
|
| 36 |
-
break
|
| 37 |
-
elif param_info.get("type") == "number" and not param_info.get("required", False):
|
| 38 |
-
test_params[param_name] = 1
|
| 39 |
-
break
|
| 40 |
-
|
| 41 |
-
# Make a test call
|
| 42 |
-
request = self.catalog_utils.format_tool_request(tool_name, test_params)
|
| 43 |
-
result = await client.call("tools/call", request["params"])
|
| 44 |
-
|
| 45 |
-
# Check if we got a valid response (not an error)
|
| 46 |
-
return "error" not in result
|
| 47 |
-
|
| 48 |
-
except Exception as e:
|
| 49 |
-
print(f"❌ Tool {tool_name} test failed: {e}")
|
| 50 |
-
return False
|
| 51 |
-
|
| 52 |
-
async def test_resource_functionality(self, resource_name: str) -> bool:
|
| 53 |
-
"""Test if a resource (API) is working by checking its endpoints."""
|
| 54 |
-
try:
|
| 55 |
-
analysis = self.catalog_utils.get_resource_analysis(resource_name)
|
| 56 |
-
if not analysis:
|
| 57 |
-
return False
|
| 58 |
-
|
| 59 |
-
# Check if the analysis has errors
|
| 60 |
-
if analysis.get("error"):
|
| 61 |
-
return False
|
| 62 |
-
|
| 63 |
-
# Check if we have endpoints
|
| 64 |
-
endpoints = analysis.get("endpoints", [])
|
| 65 |
-
if not endpoints:
|
| 66 |
-
return False
|
| 67 |
-
|
| 68 |
-
# Check if the API has meaningful content
|
| 69 |
-
title = analysis.get("title", "")
|
| 70 |
-
description = analysis.get("description", "")
|
| 71 |
-
|
| 72 |
-
# If it's a basic resource without API content, consider it working
|
| 73 |
-
if not title and not description and len(endpoints) == 0:
|
| 74 |
-
return True
|
| 75 |
-
|
| 76 |
-
# For API resources, check if we have valid endpoints
|
| 77 |
-
return len(endpoints) > 0
|
| 78 |
-
|
| 79 |
-
except Exception as e:
|
| 80 |
-
print(f"❌ Resource {resource_name} test failed: {e}")
|
| 81 |
-
return False
|
| 82 |
-
|
| 83 |
-
def generate_compact_tool_spec(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
| 84 |
-
"""Generate compact spec for a tool."""
|
| 85 |
-
schema = self.catalog_utils.get_tool_schema(tool_name)
|
| 86 |
-
if not schema:
|
| 87 |
-
return None
|
| 88 |
-
|
| 89 |
-
description = schema.get("description", "")
|
| 90 |
-
input_schema = schema.get("inputSchema", {})
|
| 91 |
-
properties = input_schema.get("properties", {})
|
| 92 |
-
required = input_schema.get("required", [])
|
| 93 |
-
|
| 94 |
-
# Extract ALL parameters with comprehensive details
|
| 95 |
-
parameters = {}
|
| 96 |
-
for param_name, param_info in properties.items():
|
| 97 |
-
param_type = param_info.get("type", "string")
|
| 98 |
-
param_desc = param_info.get("description", "")
|
| 99 |
-
param_enum = param_info.get("enum", [])
|
| 100 |
-
param_required = param_name in required
|
| 101 |
-
param_items = param_info.get("items", {})
|
| 102 |
-
param_format = param_info.get("format", "")
|
| 103 |
-
param_default = param_info.get("default")
|
| 104 |
-
|
| 105 |
-
# Build comprehensive parameter description
|
| 106 |
-
desc_parts = []
|
| 107 |
-
|
| 108 |
-
# Add base description
|
| 109 |
-
if param_desc:
|
| 110 |
-
base_desc = param_desc.split('.')[0] if '.' in param_desc else param_desc
|
| 111 |
-
desc_parts.append(base_desc)
|
| 112 |
-
|
| 113 |
-
# Add type information
|
| 114 |
-
if param_type == "array" and param_items:
|
| 115 |
-
item_type = param_items.get("type", "string")
|
| 116 |
-
desc_parts.append(f"Array of {item_type}")
|
| 117 |
-
else:
|
| 118 |
-
desc_parts.append(f"Type: {param_type}")
|
| 119 |
-
|
| 120 |
-
# Add format if specified
|
| 121 |
-
if param_format:
|
| 122 |
-
desc_parts.append(f"Format: {param_format}")
|
| 123 |
-
|
| 124 |
-
# Add required/optional status
|
| 125 |
-
desc_parts.append("Required" if param_required else "Optional")
|
| 126 |
-
|
| 127 |
-
# Add enum values if available
|
| 128 |
-
if param_enum:
|
| 129 |
-
enum_str = ", ".join([f'"{val}"' for val in param_enum[:8]]) # Show up to 8 values
|
| 130 |
-
if len(param_enum) > 8:
|
| 131 |
-
enum_str += f"... (and {len(param_enum) - 8} more)"
|
| 132 |
-
desc_parts.append(f"Valid values: {enum_str}")
|
| 133 |
-
|
| 134 |
-
# Add default value if specified
|
| 135 |
-
if param_default is not None:
|
| 136 |
-
desc_parts.append(f"Default: {param_default}")
|
| 137 |
-
|
| 138 |
-
# Combine all parts
|
| 139 |
-
desc = ". ".join(desc_parts)
|
| 140 |
-
|
| 141 |
-
# Clean up description for Python string - escape all special characters
|
| 142 |
-
desc = (desc.replace('\\', '\\\\') # Escape backslashes first
|
| 143 |
-
.replace('"', '\\"') # Escape quotes
|
| 144 |
-
.replace('\n', ' ') # Replace newlines
|
| 145 |
-
.replace('\r', ' ') # Replace carriage returns
|
| 146 |
-
.replace('`', "'")) # Replace backticks with single quotes
|
| 147 |
-
parameters[param_name] = desc
|
| 148 |
-
|
| 149 |
-
# Clean up description for Python string
|
| 150 |
-
clean_description = description[:200] + "..." if len(description) > 200 else description
|
| 151 |
-
clean_description = clean_description.replace('"', '\\"').replace('\n', ' ').replace('\r', ' ')
|
| 152 |
-
|
| 153 |
-
return {
|
| 154 |
-
"description": clean_description,
|
| 155 |
-
"parameters": parameters
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
def _extract_endpoint_parameters(self, endpoint: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
| 159 |
-
"""Extract comprehensive parameter information from an OpenAPI endpoint."""
|
| 160 |
-
if not endpoint:
|
| 161 |
-
return {}
|
| 162 |
-
|
| 163 |
-
parameters_info = {}
|
| 164 |
-
endpoint_params = endpoint.get("parameters", [])
|
| 165 |
-
|
| 166 |
-
for param in endpoint_params:
|
| 167 |
-
param_name = param.get("name", "")
|
| 168 |
-
param_in = param.get("in", "")
|
| 169 |
-
param_type = param.get("type", "string")
|
| 170 |
-
param_desc = param.get("description", "")
|
| 171 |
-
param_required = param.get("required", False)
|
| 172 |
-
param_enum = param.get("enum", [])
|
| 173 |
-
param_format = param.get("format", "")
|
| 174 |
-
param_default = param.get("default")
|
| 175 |
-
|
| 176 |
-
# Skip path parameters for base URL approach
|
| 177 |
-
if param_in == "path":
|
| 178 |
-
continue
|
| 179 |
-
|
| 180 |
-
# Skip body parameters as they're not query params
|
| 181 |
-
if param_in == "body":
|
| 182 |
-
continue
|
| 183 |
-
|
| 184 |
-
# Build comprehensive parameter description
|
| 185 |
-
desc_parts = []
|
| 186 |
-
|
| 187 |
-
# Add base description
|
| 188 |
-
if param_desc:
|
| 189 |
-
# Clean up description - remove excessive details but keep key info
|
| 190 |
-
clean_desc = param_desc.replace('\n', ' ').replace('\r', ' ')
|
| 191 |
-
if len(clean_desc) > 200:
|
| 192 |
-
# Take first 200 chars but try to end at sentence
|
| 193 |
-
truncated = clean_desc[:200]
|
| 194 |
-
last_period = truncated.rfind('.')
|
| 195 |
-
if last_period > 100: # If there's a period in reasonable range
|
| 196 |
-
clean_desc = truncated[:last_period + 1]
|
| 197 |
-
else:
|
| 198 |
-
clean_desc = truncated + "..."
|
| 199 |
-
desc_parts.append(clean_desc)
|
| 200 |
-
|
| 201 |
-
# Add type and location information
|
| 202 |
-
desc_parts.append(f"Type: {param_type}")
|
| 203 |
-
if param_in:
|
| 204 |
-
desc_parts.append(f"Location: {param_in}")
|
| 205 |
-
|
| 206 |
-
# Add required/optional status
|
| 207 |
-
desc_parts.append("Required" if param_required else "Optional")
|
| 208 |
-
|
| 209 |
-
# Add format if specified
|
| 210 |
-
if param_format:
|
| 211 |
-
desc_parts.append(f"Format: {param_format}")
|
| 212 |
-
|
| 213 |
-
# Add enum values if available
|
| 214 |
-
if param_enum:
|
| 215 |
-
enum_str = ", ".join([f'"{val}"' for val in param_enum[:6]]) # Show up to 6 values
|
| 216 |
-
if len(param_enum) > 6:
|
| 217 |
-
enum_str += f"... (and {len(param_enum) - 6} more)"
|
| 218 |
-
desc_parts.append(f"Valid values: {enum_str}")
|
| 219 |
-
|
| 220 |
-
# Add default value if specified
|
| 221 |
-
if param_default is not None:
|
| 222 |
-
desc_parts.append(f"Default: {param_default}")
|
| 223 |
-
|
| 224 |
-
# Combine all parts
|
| 225 |
-
desc = ". ".join(desc_parts)
|
| 226 |
-
|
| 227 |
-
# Clean up description for Python string - escape all special characters
|
| 228 |
-
desc = (desc.replace('\\', '\\\\') # Escape backslashes first
|
| 229 |
-
.replace('"', '\\"') # Escape quotes
|
| 230 |
-
.replace('\n', ' ') # Replace newlines
|
| 231 |
-
.replace('\r', ' ') # Replace carriage returns
|
| 232 |
-
.replace('`', "'")) # Replace backticks with single quotes
|
| 233 |
-
|
| 234 |
-
if param_name:
|
| 235 |
-
parameters_info[param_name] = desc
|
| 236 |
-
|
| 237 |
-
return parameters_info
|
| 238 |
-
|
| 239 |
-
def generate_compact_resource_spec(self, resource_name: str) -> Optional[Dict[str, Any]]:
|
| 240 |
-
"""Generate compact spec for a resource."""
|
| 241 |
-
analysis = self.catalog_utils.get_resource_analysis(resource_name)
|
| 242 |
-
if not analysis:
|
| 243 |
-
return None
|
| 244 |
-
|
| 245 |
-
title = analysis.get("title", "")
|
| 246 |
-
description = analysis.get("description", "")
|
| 247 |
-
endpoints = analysis.get("endpoints", [])
|
| 248 |
-
base_url = analysis.get("base_url", "")
|
| 249 |
-
|
| 250 |
-
# Filter out authentication-required endpoints if configured
|
| 251 |
-
if EXCLUDE_AUTH_REQUIRED_ENDPOINTS:
|
| 252 |
-
original_count = len(endpoints)
|
| 253 |
-
filtered_endpoints = []
|
| 254 |
-
|
| 255 |
-
for e in endpoints:
|
| 256 |
-
requires_auth = e.get("requires_auth", False)
|
| 257 |
-
|
| 258 |
-
# Apply different filtering strategies based on configuration
|
| 259 |
-
should_exclude = False
|
| 260 |
-
|
| 261 |
-
if EXCLUDE_401_RESPONSES and requires_auth:
|
| 262 |
-
should_exclude = True
|
| 263 |
-
elif STRICT_AUTH_FILTERING:
|
| 264 |
-
# More strict filtering - exclude if any auth indicators found
|
| 265 |
-
description = e.get("description", "").lower()
|
| 266 |
-
summary = e.get("summary", "").lower()
|
| 267 |
-
auth_keywords = ["authentication", "authorization", "bearer", "token", "auth", "login", "credential", "secured", "private", "admin"]
|
| 268 |
-
should_exclude = any(keyword in description or keyword in summary for keyword in auth_keywords)
|
| 269 |
-
|
| 270 |
-
if not should_exclude:
|
| 271 |
-
filtered_endpoints.append(e)
|
| 272 |
-
|
| 273 |
-
filtered_count = len(filtered_endpoints)
|
| 274 |
-
excluded_count = original_count - filtered_count
|
| 275 |
-
print(f" 📊 Filtered {excluded_count} auth-required endpoints from {resource_name} ({original_count} → {filtered_count})")
|
| 276 |
-
else:
|
| 277 |
-
filtered_endpoints = endpoints
|
| 278 |
-
|
| 279 |
-
# Extract key endpoints based on user requirements (base URL approach)
|
| 280 |
-
key_endpoints = []
|
| 281 |
-
|
| 282 |
-
# For Member API - use base /members endpoint with comprehensive parameters
|
| 283 |
-
if "member" in resource_name.lower():
|
| 284 |
-
# Find the main /members endpoint - check both filtered and unfiltered endpoints
|
| 285 |
-
# since /members is known to work without auth despite having 401 responses
|
| 286 |
-
members_endpoint = None
|
| 287 |
-
|
| 288 |
-
# First check filtered endpoints
|
| 289 |
-
for endpoint in filtered_endpoints:
|
| 290 |
-
if endpoint.get("path") == "/members" and endpoint.get("method") == "GET":
|
| 291 |
-
members_endpoint = endpoint
|
| 292 |
-
break
|
| 293 |
-
|
| 294 |
-
# If not found in filtered, check original endpoints (known working case)
|
| 295 |
-
if not members_endpoint:
|
| 296 |
-
for endpoint in endpoints:
|
| 297 |
-
if endpoint.get("path") == "/members" and endpoint.get("method") == "GET":
|
| 298 |
-
# Special case: /members works without auth despite 401 responses
|
| 299 |
-
if "authentication credential is optional" in endpoint.get("description", "").lower():
|
| 300 |
-
members_endpoint = endpoint
|
| 301 |
-
break
|
| 302 |
-
|
| 303 |
-
# Extract comprehensive parameter information
|
| 304 |
-
params_info = self._extract_endpoint_parameters(members_endpoint) if members_endpoint else {}
|
| 305 |
-
params_desc = f"Available parameters: {', '.join(params_info.keys())}" if params_info else "Get members list with optional filters like handle, page, perPage"
|
| 306 |
-
|
| 307 |
-
key_endpoints.append({
|
| 308 |
-
"path": "/members",
|
| 309 |
-
"method": "GET",
|
| 310 |
-
"full_url": f"{base_url}/members",
|
| 311 |
-
"description": params_desc,
|
| 312 |
-
"parameters": params_info
|
| 313 |
-
})
|
| 314 |
-
|
| 315 |
-
# For Challenges API - use base /challenges endpoint with comprehensive parameters
|
| 316 |
-
elif "challenge" in resource_name.lower():
|
| 317 |
-
# Find the main /challenges endpoint to extract real parameters
|
| 318 |
-
challenges_endpoint = None
|
| 319 |
-
for endpoint in filtered_endpoints:
|
| 320 |
-
if endpoint.get("path") == "/challenges" and endpoint.get("method") == "GET":
|
| 321 |
-
challenges_endpoint = endpoint
|
| 322 |
-
break
|
| 323 |
-
|
| 324 |
-
# Extract comprehensive parameter information
|
| 325 |
-
params_info = self._extract_endpoint_parameters(challenges_endpoint) if challenges_endpoint else {}
|
| 326 |
-
params_desc = f"Available parameters: {', '.join(params_info.keys())}" if params_info else "Get challenges list with optional filters like status, track, page, perPage"
|
| 327 |
-
|
| 328 |
-
key_endpoints.append({
|
| 329 |
-
"path": "/challenges",
|
| 330 |
-
"method": "GET",
|
| 331 |
-
"full_url": f"{base_url}/challenges",
|
| 332 |
-
"description": params_desc,
|
| 333 |
-
"parameters": params_info
|
| 334 |
-
})
|
| 335 |
-
|
| 336 |
-
# For other APIs - extract important base endpoints
|
| 337 |
-
else:
|
| 338 |
-
seen_base_paths = set()
|
| 339 |
-
for endpoint in filtered_endpoints[:5]: # Limit to first 5
|
| 340 |
-
path = endpoint.get("path", "")
|
| 341 |
-
method = endpoint.get("method", "GET")
|
| 342 |
-
summary = endpoint.get("summary", "")
|
| 343 |
-
full_url = endpoint.get("full_url", "")
|
| 344 |
-
|
| 345 |
-
# Extract base path (remove path parameters)
|
| 346 |
-
base_path = path.split('/')[1] if '/' in path and len(path.split('/')) > 1 else path
|
| 347 |
-
base_path = f"/{base_path}" if not base_path.startswith('/') else base_path
|
| 348 |
-
|
| 349 |
-
if base_path not in seen_base_paths and path and method:
|
| 350 |
-
seen_base_paths.add(base_path)
|
| 351 |
-
key_endpoints.append({
|
| 352 |
-
"path": path,
|
| 353 |
-
"method": method,
|
| 354 |
-
"summary": summary[:100] + "..." if len(summary) > 100 else summary,
|
| 355 |
-
"full_url": full_url
|
| 356 |
-
})
|
| 357 |
-
|
| 358 |
-
# Generate compact description
|
| 359 |
-
if title and description:
|
| 360 |
-
compact_desc = f"{title}: {description[:150]}..."
|
| 361 |
-
elif title:
|
| 362 |
-
compact_desc = title
|
| 363 |
-
elif description:
|
| 364 |
-
compact_desc = description[:200] + "..."
|
| 365 |
-
else:
|
| 366 |
-
compact_desc = f"API with {len(filtered_endpoints)} endpoints"
|
| 367 |
-
|
| 368 |
-
# Clean up description for Python string
|
| 369 |
-
compact_desc = compact_desc.replace('"', '\\"').replace('\n', ' ').replace('\r', ' ')
|
| 370 |
-
|
| 371 |
-
return {
|
| 372 |
-
"description": compact_desc,
|
| 373 |
-
"key_endpoints": key_endpoints,
|
| 374 |
-
"total_endpoints": len(filtered_endpoints),
|
| 375 |
-
"base_url": base_url
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
async def generate_compact_specs(self) -> Dict[str, Any]:
|
| 379 |
-
"""Generate compact specs for all working tools and resources."""
|
| 380 |
-
|
| 381 |
-
# Test and generate tool specs
|
| 382 |
-
tools = self.catalog_utils.list_available_tools()
|
| 383 |
-
working_tools = {}
|
| 384 |
-
|
| 385 |
-
for tool_name in tools:
|
| 386 |
-
print(f" 🧪 Testing tool: {tool_name}")
|
| 387 |
-
if await self.test_tool_functionality(tool_name):
|
| 388 |
-
spec = self.generate_compact_tool_spec(tool_name)
|
| 389 |
-
if spec:
|
| 390 |
-
working_tools[tool_name] = spec
|
| 391 |
-
print(f" ✅ {tool_name} - Working")
|
| 392 |
-
else:
|
| 393 |
-
print(f" ❌ {tool_name} - Not working")
|
| 394 |
-
|
| 395 |
-
# Test and generate resource specs
|
| 396 |
-
resources = self.catalog_utils.list_available_resources()
|
| 397 |
-
working_resources = {}
|
| 398 |
-
|
| 399 |
-
for resource_name in resources:
|
| 400 |
-
print(f" 🧪 Testing resource: {resource_name}")
|
| 401 |
-
if await self.test_resource_functionality(resource_name):
|
| 402 |
-
spec = self.generate_compact_resource_spec(resource_name)
|
| 403 |
-
if spec:
|
| 404 |
-
working_resources[resource_name] = spec
|
| 405 |
-
print(f" ✅ {resource_name} - Working")
|
| 406 |
-
else:
|
| 407 |
-
print(f" ❌ {resource_name} - Not working")
|
| 408 |
-
|
| 409 |
-
return {
|
| 410 |
-
"tools": working_tools,
|
| 411 |
-
"resources": working_resources
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
def save_compact_specs(self, specs: Dict[str, Any], output_file: str = "config/compact_api_specs.py"):
|
| 415 |
-
"""Save compact specs to a Python file."""
|
| 416 |
-
with open(output_file, 'w', encoding='utf-8') as f:
|
| 417 |
-
f.write("# Generated compact API specs from MCP catalog\n")
|
| 418 |
-
f.write("# Only includes working tools and resources\n\n")
|
| 419 |
-
|
| 420 |
-
f.write("COMPACT_API_SPECS = {\n")
|
| 421 |
-
|
| 422 |
-
# Write tools
|
| 423 |
-
f.write(" # Working Tools\n")
|
| 424 |
-
for tool_name, spec in specs.get("tools", {}).items():
|
| 425 |
-
f.write(f' "{tool_name}": {{\n')
|
| 426 |
-
f.write(f' "description": "{spec["description"]}",\n')
|
| 427 |
-
f.write(" \"parameters\": {\n")
|
| 428 |
-
for param_name, param_desc in spec["parameters"].items():
|
| 429 |
-
f.write(f' "{param_name}": "{param_desc}",\n')
|
| 430 |
-
f.write(" }\n")
|
| 431 |
-
f.write(" },\n")
|
| 432 |
-
|
| 433 |
-
# Write resources
|
| 434 |
-
f.write("\n # Working Resources\n")
|
| 435 |
-
for resource_name, spec in specs.get("resources", {}).items():
|
| 436 |
-
f.write(f' "{resource_name}": {{\n')
|
| 437 |
-
f.write(f' "description": "{spec["description"]}",\n')
|
| 438 |
-
f.write(f' "total_endpoints": {spec["total_endpoints"]},\n')
|
| 439 |
-
if spec.get("base_url"):
|
| 440 |
-
f.write(f' "base_url": "{spec["base_url"]}",\n')
|
| 441 |
-
f.write(" \"key_endpoints\": [\n")
|
| 442 |
-
for endpoint in spec["key_endpoints"][:5]: # Limit to 5 key endpoints
|
| 443 |
-
endpoint_line = f' {{"path": "{endpoint["path"]}", "method": "{endpoint["method"]}"'
|
| 444 |
-
if endpoint.get("full_url"):
|
| 445 |
-
endpoint_line += f', "full_url": "{endpoint["full_url"]}"'
|
| 446 |
-
if endpoint.get("description"):
|
| 447 |
-
# Escape description for Python string
|
| 448 |
-
escaped_desc = endpoint["description"].replace('"', '\\"')
|
| 449 |
-
endpoint_line += f', "description": "{escaped_desc}"'
|
| 450 |
-
if endpoint.get("parameters"):
|
| 451 |
-
# Write parameters as a nested dictionary
|
| 452 |
-
endpoint_line += ', "parameters": {'
|
| 453 |
-
param_entries = []
|
| 454 |
-
for param_name, param_desc in endpoint["parameters"].items():
|
| 455 |
-
# Escape all special characters for Python string
|
| 456 |
-
escaped_param_desc = (param_desc.replace('\\', '\\\\') # Escape backslashes first
|
| 457 |
-
.replace('"', '\\"') # Escape quotes
|
| 458 |
-
.replace('\n', ' ') # Replace newlines
|
| 459 |
-
.replace('\r', ' ') # Replace carriage returns
|
| 460 |
-
.replace('`', "'")) # Replace backticks with single quotes
|
| 461 |
-
param_entries.append(f'"{param_name}": "{escaped_param_desc}"')
|
| 462 |
-
endpoint_line += ', '.join(param_entries)
|
| 463 |
-
endpoint_line += '}'
|
| 464 |
-
endpoint_line += "},\n"
|
| 465 |
-
f.write(endpoint_line)
|
| 466 |
-
f.write(" ]\n")
|
| 467 |
-
f.write(" },\n")
|
| 468 |
-
|
| 469 |
-
f.write("}\n")
|
| 470 |
-
|
| 471 |
-
print(f"✅ Compact specs saved to {output_file}")
|
| 472 |
-
|
| 473 |
-
def print_summary(self, specs: Dict[str, Any]):
|
| 474 |
-
"""Print a summary of the generated specs."""
|
| 475 |
-
tools = specs.get("tools", {})
|
| 476 |
-
resources = specs.get("resources", {})
|
| 477 |
-
|
| 478 |
-
print(f"\n📊 Compact Specs Summary:")
|
| 479 |
-
print(f" 🔧 Working Tools: {len(tools)}")
|
| 480 |
-
for tool_name in tools:
|
| 481 |
-
print(f" - {tool_name}")
|
| 482 |
-
|
| 483 |
-
print(f" 📚 Working Resources: {len(resources)}")
|
| 484 |
-
for resource_name in resources:
|
| 485 |
-
total_endpoints = resources[resource_name].get("total_endpoints", 0)
|
| 486 |
-
print(f" - {resource_name} ({total_endpoints} endpoints)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/infrastructure/logging/logger.py
DELETED
|
@@ -1,164 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Structured logging configuration with support for different output formats.
|
| 3 |
-
Provides context-aware logging for better observability.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
import logging.handlers
|
| 8 |
-
import json
|
| 9 |
-
import sys
|
| 10 |
-
from typing import Any, Dict, Optional
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
from datetime import datetime
|
| 13 |
-
|
| 14 |
-
from config.settings import LoggingSettings
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
class StructuredFormatter(logging.Formatter):
|
| 18 |
-
"""JSON formatter for structured logging."""
|
| 19 |
-
|
| 20 |
-
def format(self, record: logging.LogRecord) -> str:
|
| 21 |
-
"""Format log record as JSON."""
|
| 22 |
-
log_data = {
|
| 23 |
-
"timestamp": datetime.utcnow().isoformat(),
|
| 24 |
-
"level": record.levelname,
|
| 25 |
-
"logger": record.name,
|
| 26 |
-
"message": record.getMessage(),
|
| 27 |
-
"module": record.module,
|
| 28 |
-
"function": record.funcName,
|
| 29 |
-
"line": record.lineno,
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
# Add exception info if present
|
| 33 |
-
if record.exc_info:
|
| 34 |
-
log_data["exception"] = self.formatException(record.exc_info)
|
| 35 |
-
|
| 36 |
-
# Add extra fields
|
| 37 |
-
if hasattr(record, "extra"):
|
| 38 |
-
log_data.update(record.extra)
|
| 39 |
-
|
| 40 |
-
return json.dumps(log_data, default=str)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
class ContextFilter(logging.Filter):
|
| 44 |
-
"""Add contextual information to log records."""
|
| 45 |
-
|
| 46 |
-
def __init__(self, context: Optional[Dict[str, Any]] = None):
|
| 47 |
-
super().__init__()
|
| 48 |
-
self.context = context or {}
|
| 49 |
-
|
| 50 |
-
def filter(self, record: logging.LogRecord) -> bool:
|
| 51 |
-
"""Add context to the log record."""
|
| 52 |
-
for key, value in self.context.items():
|
| 53 |
-
setattr(record, key, value)
|
| 54 |
-
return True
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
class ContextLogger:
|
| 58 |
-
"""Logger wrapper with context support."""
|
| 59 |
-
|
| 60 |
-
def __init__(self, logger: logging.Logger):
|
| 61 |
-
self.logger = logger
|
| 62 |
-
self.context: Dict[str, Any] = {}
|
| 63 |
-
|
| 64 |
-
def set_context(self, **kwargs) -> None:
|
| 65 |
-
"""Set logging context."""
|
| 66 |
-
self.context.update(kwargs)
|
| 67 |
-
|
| 68 |
-
def clear_context(self) -> None:
|
| 69 |
-
"""Clear logging context."""
|
| 70 |
-
self.context.clear()
|
| 71 |
-
|
| 72 |
-
def _log_with_context(self, level: int, msg: str, *args, **kwargs) -> None:
|
| 73 |
-
"""Log with context."""
|
| 74 |
-
extra = kwargs.pop("extra", {})
|
| 75 |
-
extra.update(self.context)
|
| 76 |
-
kwargs["extra"] = extra
|
| 77 |
-
self.logger.log(level, msg, *args, **kwargs)
|
| 78 |
-
|
| 79 |
-
def debug(self, msg: str, *args, **kwargs) -> None:
|
| 80 |
-
"""Log debug message with context."""
|
| 81 |
-
self._log_with_context(logging.DEBUG, msg, *args, **kwargs)
|
| 82 |
-
|
| 83 |
-
def info(self, msg: str, *args, **kwargs) -> None:
|
| 84 |
-
"""Log info message with context."""
|
| 85 |
-
self._log_with_context(logging.INFO, msg, *args, **kwargs)
|
| 86 |
-
|
| 87 |
-
def warning(self, msg: str, *args, **kwargs) -> None:
|
| 88 |
-
"""Log warning message with context."""
|
| 89 |
-
self._log_with_context(logging.WARNING, msg, *args, **kwargs)
|
| 90 |
-
|
| 91 |
-
def error(self, msg: str, *args, **kwargs) -> None:
|
| 92 |
-
"""Log error message with context."""
|
| 93 |
-
self._log_with_context(logging.ERROR, msg, *args, **kwargs)
|
| 94 |
-
|
| 95 |
-
def critical(self, msg: str, *args, **kwargs) -> None:
|
| 96 |
-
"""Log critical message with context."""
|
| 97 |
-
self._log_with_context(logging.CRITICAL, msg, *args, **kwargs)
|
| 98 |
-
|
| 99 |
-
def exception(self, msg: str, *args, **kwargs) -> None:
|
| 100 |
-
"""Log exception with context."""
|
| 101 |
-
kwargs["exc_info"] = True
|
| 102 |
-
self.error(msg, *args, **kwargs)
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def setup_logger(
|
| 106 |
-
name: str = "topcoder_mcp_agent",
|
| 107 |
-
config: Optional[LoggingSettings] = None
|
| 108 |
-
) -> ContextLogger:
|
| 109 |
-
"""
|
| 110 |
-
Set up application logger with the given configuration.
|
| 111 |
-
|
| 112 |
-
Args:
|
| 113 |
-
name: Logger name
|
| 114 |
-
config: Logging configuration
|
| 115 |
-
|
| 116 |
-
Returns:
|
| 117 |
-
Configured context logger
|
| 118 |
-
"""
|
| 119 |
-
if config is None:
|
| 120 |
-
config = LoggingSettings()
|
| 121 |
-
|
| 122 |
-
# Create logger
|
| 123 |
-
logger = logging.getLogger(name)
|
| 124 |
-
logger.setLevel(getattr(logging, config.level.upper()))
|
| 125 |
-
|
| 126 |
-
# Clear existing handlers
|
| 127 |
-
logger.handlers.clear()
|
| 128 |
-
|
| 129 |
-
# Create formatter
|
| 130 |
-
if config.file_path:
|
| 131 |
-
# Use structured JSON formatter for file output
|
| 132 |
-
formatter = StructuredFormatter()
|
| 133 |
-
else:
|
| 134 |
-
# Use simple formatter for console output
|
| 135 |
-
formatter = logging.Formatter(config.format)
|
| 136 |
-
|
| 137 |
-
# Create handlers
|
| 138 |
-
if config.file_path:
|
| 139 |
-
# File handler with rotation
|
| 140 |
-
handler = logging.handlers.RotatingFileHandler(
|
| 141 |
-
filename=config.file_path,
|
| 142 |
-
maxBytes=config.max_file_size,
|
| 143 |
-
backupCount=config.backup_count,
|
| 144 |
-
encoding="utf-8"
|
| 145 |
-
)
|
| 146 |
-
else:
|
| 147 |
-
# Console handler
|
| 148 |
-
handler = logging.StreamHandler(sys.stdout)
|
| 149 |
-
|
| 150 |
-
handler.setFormatter(formatter)
|
| 151 |
-
handler.setLevel(getattr(logging, config.level.upper()))
|
| 152 |
-
|
| 153 |
-
# Add handler to logger
|
| 154 |
-
logger.addHandler(handler)
|
| 155 |
-
|
| 156 |
-
# Prevent propagation to root logger
|
| 157 |
-
logger.propagate = False
|
| 158 |
-
|
| 159 |
-
return ContextLogger(logger)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def get_logger(name: str) -> ContextLogger:
|
| 163 |
-
"""Get a logger instance with the given name."""
|
| 164 |
-
return ContextLogger(logging.getLogger(name))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/infrastructure/providers/long_field_manager.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import Any, Dict, List, Tuple
|
| 4 |
+
|
| 5 |
+
from config import settings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LongFieldManager:
|
| 9 |
+
"""
|
| 10 |
+
Detects long fields in tool/resource responses and manages a human/LLM-editable
|
| 11 |
+
configuration file that records which fields are considered long per tool/resource.
|
| 12 |
+
|
| 13 |
+
Behavior:
|
| 14 |
+
- By default, long fields are omitted from the data passed to the final LLM summarization.
|
| 15 |
+
- If the user's query explicitly asks for the long field(s), include them.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, config_path: str = "config/long_fields_config.json"):
|
| 19 |
+
self.config_path = config_path
|
| 20 |
+
self._config = self._load_config()
|
| 21 |
+
|
| 22 |
+
# Thresholds from settings.py (human editable via env)
|
| 23 |
+
self.max_string_chars = getattr(settings, "LONG_FIELD_STRING_MAX_CHARS", 800)
|
| 24 |
+
self.max_array_items = getattr(settings, "LONG_FIELD_ARRAY_MAX_ITEMS", 50)
|
| 25 |
+
self.max_array_bytes = getattr(settings, "LONG_FIELD_ARRAY_MAX_BYTES", 50_000)
|
| 26 |
+
|
| 27 |
+
# Simple keywords that indicate explicit request for full/long content
|
| 28 |
+
self.request_keywords = [
|
| 29 |
+
"full", "complete", "entire", "expand", "show more", "all items",
|
| 30 |
+
"full description", "whole", "everything", "detailed", "complete list"
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
# -------- Config management --------
|
| 34 |
+
def _load_config(self) -> Dict[str, Dict[str, List[str]]]:
|
| 35 |
+
if not os.path.exists(self.config_path):
|
| 36 |
+
return {"tools": {}, "resources": {}}
|
| 37 |
+
try:
|
| 38 |
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
| 39 |
+
data = json.load(f)
|
| 40 |
+
# Ensure shape
|
| 41 |
+
if not isinstance(data, dict):
|
| 42 |
+
return {"tools": {}, "resources": {}}
|
| 43 |
+
data.setdefault("tools", {})
|
| 44 |
+
data.setdefault("resources", {})
|
| 45 |
+
return data
|
| 46 |
+
except Exception:
|
| 47 |
+
return {"tools": {}, "resources": {}}
|
| 48 |
+
|
| 49 |
+
def _save_config(self) -> None:
|
| 50 |
+
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
| 51 |
+
with open(self.config_path, "w", encoding="utf-8") as f:
|
| 52 |
+
json.dump(self._config, f, indent=2, ensure_ascii=False)
|
| 53 |
+
|
| 54 |
+
def record_long_field(self, kind: str, name: str, field_path: str) -> None:
|
| 55 |
+
"""Record a long field path under a tool/resource in config."""
|
| 56 |
+
if kind not in ("tools", "resources"):
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
# Skip recording paths that would break response structure:
|
| 60 |
+
# 1. Top-level single segments (likely containers)
|
| 61 |
+
# 2. Paths that are common response containers used by filter_tool_response
|
| 62 |
+
if self._is_structural_path(field_path):
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
bucket = self._config.setdefault(kind, {})
|
| 66 |
+
fields = bucket.setdefault(name, [])
|
| 67 |
+
if field_path not in fields:
|
| 68 |
+
fields.append(field_path)
|
| 69 |
+
self._save_config()
|
| 70 |
+
|
| 71 |
+
def _is_structural_path(self, field_path: str) -> bool:
|
| 72 |
+
"""
|
| 73 |
+
Determine if a field path is structural (container) and should not be redacted.
|
| 74 |
+
Structural paths are those that contain the main response data.
|
| 75 |
+
"""
|
| 76 |
+
# Only consider actual container names as structural
|
| 77 |
+
# These are the top-level response containers, not field names
|
| 78 |
+
if field_path in ("data", "items", "records", "results", "members", "challenges"):
|
| 79 |
+
return True
|
| 80 |
+
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
def get_known_long_fields(self, kind: str, name: str) -> List[str]:
|
| 84 |
+
return self._config.get(kind, {}).get(name, [])
|
| 85 |
+
|
| 86 |
+
# -------- Detection --------
|
| 87 |
+
def _is_long_string(self, value: Any) -> bool:
|
| 88 |
+
return isinstance(value, str) and len(value) > self.max_string_chars
|
| 89 |
+
|
| 90 |
+
def _serialized_size(self, value: Any) -> int:
|
| 91 |
+
try:
|
| 92 |
+
return len(json.dumps(value, ensure_ascii=False))
|
| 93 |
+
except Exception:
|
| 94 |
+
return 0
|
| 95 |
+
|
| 96 |
+
def _is_long_array(self, value: Any) -> bool:
|
| 97 |
+
if not isinstance(value, list):
|
| 98 |
+
return False
|
| 99 |
+
# Check multiple criteria for "long" arrays
|
| 100 |
+
if len(value) > self.max_array_items:
|
| 101 |
+
return True
|
| 102 |
+
if self._serialized_size(value) > self.max_array_bytes:
|
| 103 |
+
return True
|
| 104 |
+
# For string arrays, also check total character count
|
| 105 |
+
if value and all(isinstance(x, str) for x in value):
|
| 106 |
+
total_chars = sum(len(x) for x in value)
|
| 107 |
+
if total_chars > self.max_string_chars:
|
| 108 |
+
return True
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
def _collect_long_fields(self, data: Any, parent_path: str = "") -> List[Tuple[str, str]]:
|
| 112 |
+
"""
|
| 113 |
+
Traverse data and collect (path, kind) where kind in {"string","array"} is long.
|
| 114 |
+
Paths are expressed as dot paths; list indices are not included to keep paths stable.
|
| 115 |
+
"""
|
| 116 |
+
found: List[Tuple[str, str]] = []
|
| 117 |
+
|
| 118 |
+
if isinstance(data, dict):
|
| 119 |
+
for k, v in data.items():
|
| 120 |
+
path = f"{parent_path}.{k}" if parent_path else k
|
| 121 |
+
if self._is_long_string(v):
|
| 122 |
+
found.append((path, "string"))
|
| 123 |
+
elif self._is_long_array(v):
|
| 124 |
+
found.append((path, "array"))
|
| 125 |
+
else:
|
| 126 |
+
# Recurse into nested structures
|
| 127 |
+
found.extend(self._collect_long_fields(v, path))
|
| 128 |
+
elif isinstance(data, list):
|
| 129 |
+
# Check if the list itself is long
|
| 130 |
+
if self._is_long_array(data):
|
| 131 |
+
found.append((parent_path, "array"))
|
| 132 |
+
else:
|
| 133 |
+
# For arrays, examine first few items to find nested long fields
|
| 134 |
+
for i, item in enumerate(data[:3]): # Sample first 3 items
|
| 135 |
+
if isinstance(item, dict):
|
| 136 |
+
# For dict items in arrays, check their fields
|
| 137 |
+
for k, v in item.items():
|
| 138 |
+
# For array items, use just the field name, not parent_path.field_name
|
| 139 |
+
# This matches how the redaction will work
|
| 140 |
+
field_path = k
|
| 141 |
+
if self._is_long_string(v):
|
| 142 |
+
found.append((field_path, "string"))
|
| 143 |
+
elif self._is_long_array(v):
|
| 144 |
+
found.append((field_path, "array"))
|
| 145 |
+
else:
|
| 146 |
+
# Recurse deeper
|
| 147 |
+
found.extend(self._collect_long_fields(v, field_path))
|
| 148 |
+
|
| 149 |
+
return found
|
| 150 |
+
|
| 151 |
+
# -------- Query intent for long fields --------
|
| 152 |
+
def _query_requests_long_fields(self, query: str, field_paths: List[str]) -> bool:
|
| 153 |
+
q = (query or "").lower()
|
| 154 |
+
# Keyword signal
|
| 155 |
+
if any(kw in q for kw in self.request_keywords):
|
| 156 |
+
return True
|
| 157 |
+
# Direct field-name signal: match last segment of a path
|
| 158 |
+
last_segments = {p.split(".")[-1].lower() for p in field_paths}
|
| 159 |
+
return any(seg in q for seg in last_segments if seg)
|
| 160 |
+
|
| 161 |
+
# -------- Public API --------
|
| 162 |
+
def prepare_data_for_prompt(
|
| 163 |
+
self,
|
| 164 |
+
kind: str, # "tools" or "resources"
|
| 165 |
+
name: str,
|
| 166 |
+
data: Any,
|
| 167 |
+
user_query: str,
|
| 168 |
+
) -> Any:
|
| 169 |
+
"""
|
| 170 |
+
Return data with long fields omitted unless the query explicitly requests them.
|
| 171 |
+
Also records detected long fields into the editable config file.
|
| 172 |
+
"""
|
| 173 |
+
if not isinstance(data, (dict, list)):
|
| 174 |
+
return data
|
| 175 |
+
|
| 176 |
+
# Detect long fields present in this payload
|
| 177 |
+
detected = self._collect_long_fields(data)
|
| 178 |
+
detected_paths = [p for p, _ in detected]
|
| 179 |
+
|
| 180 |
+
# Persist discovered long fields (with structural filtering)
|
| 181 |
+
recorded_paths = []
|
| 182 |
+
for p in detected_paths:
|
| 183 |
+
if not self._is_structural_path(p):
|
| 184 |
+
self.record_long_field(kind, name, p)
|
| 185 |
+
recorded_paths.append(p)
|
| 186 |
+
|
| 187 |
+
# Combine with known long fields from config
|
| 188 |
+
known_paths = self.get_known_long_fields(kind, name)
|
| 189 |
+
all_long_paths = sorted(set(recorded_paths + known_paths))
|
| 190 |
+
|
| 191 |
+
if not all_long_paths:
|
| 192 |
+
return data
|
| 193 |
+
|
| 194 |
+
# Decide whether to include long fields based on user query
|
| 195 |
+
include_long = self._query_requests_long_fields(user_query, all_long_paths)
|
| 196 |
+
|
| 197 |
+
if include_long:
|
| 198 |
+
return data # keep as-is
|
| 199 |
+
|
| 200 |
+
# Otherwise, remove or redact long fields from data
|
| 201 |
+
return self._redact_long_fields(data, set(all_long_paths))
|
| 202 |
+
|
| 203 |
+
def _redact_long_fields(self, data: Any, long_paths: set, parent_path: str = "") -> Any:
|
| 204 |
+
"""
|
| 205 |
+
Redact only the exact long field paths, not their ancestors.
|
| 206 |
+
This preserves containers (e.g., keep 'members' list, but redact 'members.skills').
|
| 207 |
+
"""
|
| 208 |
+
# If the current container itself is the long field, replace it wholesale
|
| 209 |
+
if parent_path and parent_path in long_paths:
|
| 210 |
+
return {"_omitted": True, "reason": "long_field", "path": parent_path}
|
| 211 |
+
|
| 212 |
+
if isinstance(data, dict):
|
| 213 |
+
out = {}
|
| 214 |
+
for k, v in data.items():
|
| 215 |
+
path = f"{parent_path}.{k}" if parent_path else k
|
| 216 |
+
if path in long_paths:
|
| 217 |
+
out[k] = {"_omitted": True, "reason": "long_field", "path": path}
|
| 218 |
+
else:
|
| 219 |
+
out[k] = self._redact_long_fields(v, long_paths, path)
|
| 220 |
+
return out
|
| 221 |
+
elif isinstance(data, list):
|
| 222 |
+
# Check if this list itself should be redacted
|
| 223 |
+
if parent_path in long_paths:
|
| 224 |
+
return {"_omitted": True, "reason": "long_field", "path": parent_path}
|
| 225 |
+
|
| 226 |
+
# Otherwise, process each item in the list
|
| 227 |
+
result = []
|
| 228 |
+
for item in data:
|
| 229 |
+
if isinstance(item, dict):
|
| 230 |
+
# For dict items, check each field for redaction
|
| 231 |
+
redacted_item = {}
|
| 232 |
+
for k, v in item.items():
|
| 233 |
+
# For array items, check against the field name directly
|
| 234 |
+
if k in long_paths:
|
| 235 |
+
redacted_item[k] = {"_omitted": True, "reason": "long_field", "path": k}
|
| 236 |
+
else:
|
| 237 |
+
redacted_item[k] = self._redact_long_fields(v, long_paths, k)
|
| 238 |
+
result.append(redacted_item)
|
| 239 |
+
else:
|
| 240 |
+
result.append(self._redact_long_fields(item, long_paths, parent_path))
|
| 241 |
+
return result
|
| 242 |
+
return data
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# Global singleton
|
| 246 |
+
long_field_manager = LongFieldManager()
|
| 247 |
+
|
src/infrastructure/providers/response_filter.py
CHANGED
|
@@ -417,25 +417,30 @@ def _filter_parsed_data(tool: str, data: dict | list) -> dict | list:
|
|
| 417 |
}
|
| 418 |
|
| 419 |
# Handle member-related tools
|
| 420 |
-
|
|
|
|
| 421 |
if isinstance(data, dict):
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
elif isinstance(data, list):
|
| 433 |
return {
|
| 434 |
"summary": {
|
| 435 |
-
"
|
| 436 |
-
"
|
| 437 |
},
|
| 438 |
-
"
|
| 439 |
}
|
| 440 |
|
| 441 |
# Default fallback - return data as is but limit lists
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
# Handle member-related tools
|
| 420 |
+
else:
|
| 421 |
+
# Generic handling for non-specialized tools/resources
|
| 422 |
if isinstance(data, dict):
|
| 423 |
+
# Prefer common list containers
|
| 424 |
+
for key in ("data", "items", "records", "results"):
|
| 425 |
+
val = data.get(key)
|
| 426 |
+
if isinstance(val, list):
|
| 427 |
+
lst = val
|
| 428 |
+
return {
|
| 429 |
+
"summary": {
|
| 430 |
+
"total_items": len(lst),
|
| 431 |
+
"items_returned": min(5, len(lst))
|
| 432 |
+
},
|
| 433 |
+
"items": lst[:5]
|
| 434 |
+
}
|
| 435 |
+
# If a single object, just return as-is
|
| 436 |
+
return data
|
| 437 |
elif isinstance(data, list):
|
| 438 |
return {
|
| 439 |
"summary": {
|
| 440 |
+
"total_items": len(data),
|
| 441 |
+
"items_returned": min(5, len(data))
|
| 442 |
},
|
| 443 |
+
"items": data[:5]
|
| 444 |
}
|
| 445 |
|
| 446 |
# Default fallback - return data as is but limit lists
|
src/presentation/gradio_interface.py
CHANGED
|
@@ -9,6 +9,7 @@ from config.system_prompts import topcoder_system_prompt
|
|
| 9 |
from config.static_responses import REJECT_RESPONSE
|
| 10 |
from config.prompt_templates import PromptTemplates
|
| 11 |
from src.infrastructure.providers.response_filter import filter_tool_response
|
|
|
|
| 12 |
|
| 13 |
llm_client = LLMClient()
|
| 14 |
tool_executor = ToolExecutor()
|
|
@@ -111,6 +112,22 @@ async def agent_response(user_message, history):
|
|
| 111 |
# STEP 8: Filter response before summarizing
|
| 112 |
filtered_data = filter_tool_response(target_name, tool_result.data)
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
# Build dynamic summarization prompt based on the target type
|
| 115 |
if target_type == "tool":
|
| 116 |
context = f"Tool used: {target_name}"
|
|
@@ -118,7 +135,7 @@ async def agent_response(user_message, history):
|
|
| 118 |
context = f"Resource used: {target_name}"
|
| 119 |
|
| 120 |
summarization_prompt = PromptTemplates.get_gradio_response_summarization_prompt(
|
| 121 |
-
user_message, context,
|
| 122 |
)
|
| 123 |
|
| 124 |
final_response = await llm_client.chat([
|
|
|
|
| 9 |
from config.static_responses import REJECT_RESPONSE
|
| 10 |
from config.prompt_templates import PromptTemplates
|
| 11 |
from src.infrastructure.providers.response_filter import filter_tool_response
|
| 12 |
+
from src.infrastructure.providers.long_field_manager import long_field_manager
|
| 13 |
|
| 14 |
llm_client = LLMClient()
|
| 15 |
tool_executor = ToolExecutor()
|
|
|
|
| 112 |
# STEP 8: Filter response before summarizing
|
| 113 |
filtered_data = filter_tool_response(target_name, tool_result.data)
|
| 114 |
|
| 115 |
+
# Avoid hardcoding member-specific wrapping; rely on generic filter output
|
| 116 |
+
|
| 117 |
+
# STEP 8b: Omit long fields by default unless the query explicitly asks for them.
|
| 118 |
+
# Map target kind for config bucketing
|
| 119 |
+
kind = "tools" if target_type == "tool" else "resources"
|
| 120 |
+
filtered_or_redacted = long_field_manager.prepare_data_for_prompt(
|
| 121 |
+
kind=kind,
|
| 122 |
+
name=target_name,
|
| 123 |
+
data=filtered_data,
|
| 124 |
+
user_query=user_message,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Ensure we never return empty data due to redaction; keep a minimal summary fallback
|
| 128 |
+
if not filtered_or_redacted or (isinstance(filtered_or_redacted, dict) and len(filtered_or_redacted.keys()) == 0):
|
| 129 |
+
filtered_or_redacted = {"summary": "Data available with long fields omitted by default.", "context": context}
|
| 130 |
+
|
| 131 |
# Build dynamic summarization prompt based on the target type
|
| 132 |
if target_type == "tool":
|
| 133 |
context = f"Tool used: {target_name}"
|
|
|
|
| 135 |
context = f"Resource used: {target_name}"
|
| 136 |
|
| 137 |
summarization_prompt = PromptTemplates.get_gradio_response_summarization_prompt(
|
| 138 |
+
user_message, context, filtered_or_redacted
|
| 139 |
)
|
| 140 |
|
| 141 |
final_response = await llm_client.chat([
|