abhishekrn commited on
Commit
b863252
·
1 Parent(s): a57d85f

long response handling

Browse files
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 accurate information based on the actual data.
91
 
92
- The user asked: "{user_message}"
93
  {context}
94
 
95
- Response data:
96
  {filtered_data}
97
 
98
- Instructions:
99
- 1. If the data shows results exist, say so with the actual numbers
100
- 2. If the data shows no results, say "No results found"
101
- 3. Don't make up information - only use what's in the data
102
- 4. If there are multiple items, mention the total count and show a few examples
103
- 5. Be specific about what was found vs what was requested
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
- elif "member" in tool.lower():
 
421
  if isinstance(data, dict):
422
- members = data.get("data", [])
423
- total = data.get("total", 0)
424
-
425
- return {
426
- "summary": {
427
- "total_members": total,
428
- "members_returned": len(members)
429
- },
430
- "members": members[:5] if isinstance(members, list) else []
431
- }
 
 
 
 
432
  elif isinstance(data, list):
433
  return {
434
  "summary": {
435
- "total_members": len(data),
436
- "members_returned": min(5, len(data))
437
  },
438
- "members": data[:5]
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, filtered_data
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([