This is page 3 of 4. Use http://codebase.md/angrysky56/mcts-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .gitignore
├── archive
│ ├── ANALYSIS_TOOLS.md
│ ├── First-Run.md
│ ├── fixed_tools.py
│ ├── gemini_adapter_old.py
│ ├── gemini_adapter.py
│ ├── GEMINI_SETUP.md
│ ├── QUICK_START_FIXED.md
│ ├── QUICK_START.md
│ ├── README.md
│ ├── run_test.py
│ ├── SERVER_FIX_SUMMARY.md
│ ├── setup_analysis_venv.sh
│ ├── setup_analysis.sh
│ ├── SETUP_SUMMARY.md
│ ├── test_adapter.py
│ ├── test_fixed_server.py
│ ├── test_gemini_setup.py
│ ├── test_mcp_init.py
│ ├── test_minimal.py
│ ├── test_new_adapters.py
│ ├── test_ollama.py
│ ├── test_rate_limiting.py
│ ├── test_server_debug.py
│ ├── test_server.py
│ ├── test_simple.py
│ ├── test_startup_simple.py
│ ├── test_startup.py
│ ├── TIMEOUT_FIX.md
│ ├── tools_fast.py
│ ├── tools_old.py
│ └── tools_original.py
├── image-1.png
├── image-2.png
├── image-3.png
├── image.png
├── LICENSE
├── prompts
│ ├── README.md
│ └── usage_guide.md
├── pyproject.toml
├── README.md
├── results
│ ├── cogito:32b
│ │ └── cogito:32b_1745989705
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ ├── cogito:latest
│ │ ├── cogito:latest_1745979984
│ │ │ ├── best_solution.txt
│ │ │ └── progress.jsonl
│ │ └── cogito:latest_1745984274
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ ├── local
│ │ ├── local_1745956311
│ │ │ ├── best_solution.txt
│ │ │ └── progress.jsonl
│ │ ├── local_1745956673
│ │ │ ├── best_solution.txt
│ │ │ └── progress.jsonl
│ │ └── local_1745958556
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ └── qwen3:0.6b
│ ├── qwen3:0.6b_1745960624
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ ├── qwen3:0.6b_1745960651
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ ├── qwen3:0.6b_1745960694
│ │ ├── best_solution.txt
│ │ └── progress.jsonl
│ └── qwen3:0.6b_1745977462
│ ├── best_solution.txt
│ └── progress.jsonl
├── setup_unix.sh
├── setup_windows.bat
├── setup.py
├── setup.sh
├── src
│ └── mcts_mcp_server
│ ├── __init__.py
│ ├── analysis_tools
│ │ ├── __init__.py
│ │ ├── mcts_tools.py
│ │ └── results_processor.py
│ ├── anthropic_adapter.py
│ ├── base_llm_adapter.py
│ ├── gemini_adapter.py
│ ├── intent_handler.py
│ ├── llm_adapter.py
│ ├── llm_interface.py
│ ├── manage_server.py
│ ├── mcts_config.py
│ ├── mcts_core.py
│ ├── node.py
│ ├── ollama_adapter.py
│ ├── ollama_check.py
│ ├── ollama_utils.py
│ ├── openai_adapter.py
│ ├── rate_limiter.py
│ ├── reality_warps_adapter.py
│ ├── results_collector.py
│ ├── server.py
│ ├── state_manager.py
│ ├── tools.py
│ └── utils.py
├── USAGE_GUIDE.md
├── uv.lock
└── verify_installation.py
```
# Files
--------------------------------------------------------------------------------
/src/mcts_mcp_server/server.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Simple MCTS MCP Server - Basic Implementation
4 | ============================================
5 |
6 | A working Monte Carlo Tree Search server using basic MCP server.
7 | """
8 | import asyncio
9 | import json
10 | import logging
11 | import os
12 | import sys
13 | from typing import Any
14 |
15 | import mcp.server.stdio
16 | import mcp.types as types
17 | from mcp.server import Server
18 |
19 | # Configure logging
20 | logging.basicConfig(level=logging.INFO)
21 | logger = logging.getLogger(__name__)
22 |
23 | # Create server
24 | server = Server("mcts-server")
25 |
26 | # Simple state storage
27 | server_state = {
28 | "current_question": None,
29 | "chat_id": None,
30 | "provider": "gemini",
31 | "model": "gemini-2.0-flash-lite",
32 | "iterations_completed": 0,
33 | "best_score": 0.0,
34 | "best_analysis": "",
35 | "initialized": False
36 | }
37 |
38 | def get_gemini_client():
39 | """Get a Gemini client if API key is available."""
40 | try:
41 | from google import genai
42 |
43 | api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
44 | if not api_key:
45 | return None
46 |
47 | client = genai.Client(api_key=api_key)
48 | return client
49 | except Exception as e:
50 | logger.error(f"Failed to create Gemini client: {e}")
51 | return None
52 |
53 | async def call_llm(prompt: str) -> str:
54 | """Call the configured LLM with a prompt."""
55 | try:
56 | client = get_gemini_client()
57 | if not client:
58 | return "Error: No Gemini API key configured. Set GEMINI_API_KEY environment variable."
59 |
60 | # Use the async API properly
61 | response = await client.aio.models.generate_content(
62 | model=server_state["model"],
63 | contents=[{
64 | 'role': 'user',
65 | 'parts': [{'text': prompt}]
66 | }]
67 | )
68 |
69 | if response.candidates and len(response.candidates) > 0:
70 | candidate = response.candidates[0]
71 | if candidate.content and candidate.content.parts:
72 | text = candidate.content.parts[0].text
73 | return text if text is not None else "No response generated."
74 |
75 | return "No response generated."
76 |
77 | except Exception as e:
78 | logger.error(f"LLM call failed: {e}")
79 | return f"Error calling LLM: {e!s}"
80 |
81 | @server.list_tools()
82 | async def handle_list_tools() -> list[types.Tool]:
83 | """List available tools."""
84 | return [
85 | types.Tool(
86 | name="initialize_mcts",
87 | description="Initialize MCTS for a question",
88 | inputSchema={
89 | "type": "object",
90 | "properties": {
91 | "question": {"type": "string", "description": "The question to analyze"},
92 | "chat_id": {"type": "string", "description": "Unique identifier for this conversation", "default": "default"},
93 | "provider": {"type": "string", "description": "LLM provider", "default": "gemini"},
94 | "model": {"type": "string", "description": "Model name (optional)"}
95 | },
96 | "required": ["question"]
97 | }
98 | ),
99 | types.Tool(
100 | name="run_mcts_search",
101 | description="Run MCTS search iterations",
102 | inputSchema={
103 | "type": "object",
104 | "properties": {
105 | "iterations": {"type": "integer", "description": "Number of search iterations (1-10)", "default": 3},
106 | "simulations_per_iteration": {"type": "integer", "description": "Simulations per iteration (1-20)", "default": 5}
107 | }
108 | }
109 | ),
110 | types.Tool(
111 | name="get_synthesis",
112 | description="Generate a final synthesis of the MCTS results",
113 | inputSchema={"type": "object", "properties": {}}
114 | ),
115 | types.Tool(
116 | name="get_status",
117 | description="Get the current MCTS status",
118 | inputSchema={"type": "object", "properties": {}}
119 | ),
120 | types.Tool(
121 | name="set_provider",
122 | description="Set the LLM provider and model",
123 | inputSchema={
124 | "type": "object",
125 | "properties": {
126 | "provider": {"type": "string", "description": "Provider name", "default": "gemini"},
127 | "model": {"type": "string", "description": "Model name (optional)"}
128 | }
129 | }
130 | ),
131 | types.Tool(
132 | name="list_available_models",
133 | description="List available models for a provider",
134 | inputSchema={
135 | "type": "object",
136 | "properties": {
137 | "provider": {"type": "string", "description": "Provider name", "default": "gemini"}
138 | }
139 | }
140 | )
141 | ]
142 |
143 | @server.call_tool()
144 | async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent]:
145 | """Handle tool calls."""
146 | if arguments is None:
147 | arguments = {}
148 |
149 | try:
150 | if name == "initialize_mcts":
151 | result = await initialize_mcts(**arguments)
152 | elif name == "run_mcts_search":
153 | result = await run_mcts_search(**arguments)
154 | elif name == "get_synthesis":
155 | result = await get_synthesis()
156 | elif name == "get_status":
157 | result = get_status()
158 | elif name == "set_provider":
159 | result = set_provider(**arguments)
160 | elif name == "list_available_models":
161 | result = list_available_models(**arguments)
162 | else:
163 | result = {"error": f"Unknown tool: {name}", "status": "error"}
164 |
165 | return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
166 |
167 | except Exception as e:
168 | logger.error(f"Error calling tool {name}: {e}")
169 | error_result = {"error": f"Tool execution failed: {e!s}", "status": "error"}
170 | return [types.TextContent(type="text", text=json.dumps(error_result, indent=2))]
171 |
172 | async def initialize_mcts(
173 | question: str,
174 | chat_id: str = "default",
175 | provider: str = "gemini",
176 | model: str | None = None
177 | ) -> dict[str, Any]:
178 | """
179 | Initialize MCTS for a question.
180 |
181 | Args:
182 | question: The question or topic to analyze
183 | chat_id: Unique identifier for this conversation session
184 | provider: LLM provider to use (currently only 'gemini' supported)
185 | model: Specific model name to use (optional, defaults to gemini-2.0-flash-lite)
186 |
187 | Returns:
188 | Dict containing initialization status, configuration, and any error messages
189 |
190 | Raises:
191 | Exception: If initialization fails due to missing API key or other errors
192 | """
193 | try:
194 | # Validate inputs
195 | if not question.strip():
196 | return {"error": "Question cannot be empty", "status": "error"}
197 |
198 | if provider.lower() != "gemini":
199 | return {"error": "Only 'gemini' provider is currently supported", "status": "error"}
200 |
201 | # Check if API key is available
202 | api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
203 | if not api_key:
204 | return {
205 | "error": "GEMINI_API_KEY or GOOGLE_API_KEY environment variable required",
206 | "status": "error",
207 | "setup_help": "Set your API key with: export GEMINI_API_KEY='your-key-here'"
208 | }
209 |
210 | # Update state
211 | server_state.update({
212 | "current_question": question,
213 | "chat_id": chat_id,
214 | "provider": provider.lower(),
215 | "model": model or "gemini-2.0-flash-lite",
216 | "iterations_completed": 0,
217 | "best_score": 0.0,
218 | "best_analysis": "",
219 | "initialized": True
220 | })
221 |
222 | logger.info(f"Initialized MCTS for question: {question[:50]}...")
223 |
224 | return {
225 | "status": "initialized",
226 | "question": question,
227 | "chat_id": chat_id,
228 | "provider": server_state["provider"],
229 | "model": server_state["model"],
230 | "message": "MCTS initialized successfully. Use run_mcts_search to begin analysis."
231 | }
232 |
233 | except Exception as e:
234 | logger.error(f"Error initializing MCTS: {e}")
235 | return {"error": f"Initialization failed: {e!s}", "status": "error"}
236 |
237 | async def run_mcts_search(
238 | iterations: int = 3,
239 | simulations_per_iteration: int = 5
240 | ) -> dict[str, Any]:
241 | """
242 | Run MCTS search iterations to explore different analytical approaches.
243 |
244 | Args:
245 | iterations: Number of search iterations to run (1-10, clamped to range)
246 | simulations_per_iteration: Number of simulations per iteration (1-20, clamped to range)
247 |
248 | Returns:
249 | Dict containing search results including best analysis, scores, and statistics
250 |
251 | Raises:
252 | Exception: If search fails due to LLM errors or other issues
253 | """
254 | if not server_state["initialized"]:
255 | return {"error": "MCTS not initialized. Call initialize_mcts first.", "status": "error"}
256 |
257 | # Validate parameters
258 | iterations = max(1, min(10, iterations))
259 | simulations_per_iteration = max(1, min(20, simulations_per_iteration))
260 |
261 | try:
262 | question = server_state["current_question"]
263 |
264 | # Generate multiple analysis approaches
265 | analyses = []
266 |
267 | for i in range(iterations):
268 | logger.info(f"Running iteration {i+1}/{iterations}")
269 |
270 | for j in range(simulations_per_iteration):
271 | # Create a prompt for this simulation
272 | if i == 0 and j == 0:
273 | # Initial analysis
274 | prompt = f"""Provide a thoughtful analysis of this question: {question}
275 |
276 | Focus on being insightful, comprehensive, and offering unique perspectives."""
277 | else:
278 | # Varied approaches for subsequent simulations
279 | approaches = [
280 | "from a practical perspective",
281 | "considering potential counterarguments",
282 | "examining underlying assumptions",
283 | "exploring alternative solutions",
284 | "analyzing long-term implications",
285 | "considering different stakeholder viewpoints",
286 | "examining historical context",
287 | "thinking about implementation challenges"
288 | ]
289 | approach = approaches[(i * simulations_per_iteration + j) % len(approaches)]
290 |
291 | prompt = f"""Analyze this question {approach}: {question}
292 |
293 | Previous best analysis (score {server_state['best_score']:.1f}/10):
294 | {server_state['best_analysis'][:200]}...
295 |
296 | Provide a different angle or deeper insight."""
297 |
298 | # Get analysis
299 | analysis = await call_llm(prompt)
300 |
301 | # Score the analysis
302 | score_prompt = f"""Rate the quality and insight of this analysis on a scale of 1-10:
303 |
304 | Question: {question}
305 | Analysis: {analysis}
306 |
307 | Consider: depth, originality, practical value, logical consistency.
308 | Respond with just a number from 1-10."""
309 |
310 | score_response = await call_llm(score_prompt)
311 |
312 | # Parse score
313 | try:
314 | import re
315 | score_matches = re.findall(r'\b([1-9]|10)\b', score_response)
316 | score = float(score_matches[0]) if score_matches else 5.0
317 | except (ValueError, IndexError, TypeError):
318 | score = 5.0
319 |
320 | analyses.append({
321 | "iteration": i + 1,
322 | "simulation": j + 1,
323 | "analysis": analysis,
324 | "score": score
325 | })
326 |
327 | # Update best if this is better
328 | if score > server_state["best_score"]:
329 | server_state["best_score"] = score
330 | server_state["best_analysis"] = analysis
331 |
332 | logger.info(f"Simulation {j+1} completed with score: {score:.1f}")
333 |
334 | server_state["iterations_completed"] = i + 1
335 |
336 | # Find the best analysis
337 | best_analysis = max(analyses, key=lambda x: x["score"])
338 |
339 | return {
340 | "status": "completed",
341 | "iterations_completed": iterations,
342 | "total_simulations": len(analyses),
343 | "best_score": server_state["best_score"],
344 | "best_analysis": server_state["best_analysis"],
345 | "best_from_this_run": best_analysis,
346 | "all_scores": [a["score"] for a in analyses],
347 | "average_score": sum(a["score"] for a in analyses) / len(analyses),
348 | "provider": server_state["provider"],
349 | "model": server_state["model"]
350 | }
351 |
352 | except Exception as e:
353 | logger.error(f"Error during MCTS search: {e}")
354 | return {"error": f"Search failed: {e!s}", "status": "error"}
355 |
356 | async def get_synthesis() -> dict[str, Any]:
357 | """
358 | Generate a final synthesis of the MCTS results.
359 |
360 | Creates a comprehensive summary that synthesizes the key insights from the best
361 | analysis found during the MCTS search process.
362 |
363 | Returns:
364 | Dict containing the synthesis text, best score, and metadata
365 |
366 | Raises:
367 | Exception: If synthesis generation fails or MCTS hasn't been run yet
368 | """
369 | if not server_state["initialized"]:
370 | return {"error": "MCTS not initialized. Call initialize_mcts first.", "status": "error"}
371 |
372 | if server_state["best_score"] == 0.0:
373 | return {"error": "No analysis completed yet. Run run_mcts_search first.", "status": "error"}
374 |
375 | try:
376 | question = server_state["current_question"]
377 | best_analysis = server_state["best_analysis"]
378 | best_score = server_state["best_score"]
379 |
380 | synthesis_prompt = f"""Create a comprehensive synthesis based on this MCTS analysis:
381 |
382 | Original Question: {question}
383 |
384 | Best Analysis Found (Score: {best_score}/10):
385 | {best_analysis}
386 |
387 | Provide a final synthesis that:
388 | 1. Summarizes the key insights
389 | 2. Highlights the most important findings
390 | 3. Offers actionable conclusions
391 | 4. Explains why this approach is valuable
392 |
393 | Make it clear, comprehensive, and practical."""
394 |
395 | synthesis = await call_llm(synthesis_prompt)
396 |
397 | return {
398 | "synthesis": synthesis,
399 | "best_score": best_score,
400 | "iterations_completed": server_state["iterations_completed"],
401 | "question": question,
402 | "provider": server_state["provider"],
403 | "model": server_state["model"],
404 | "status": "success"
405 | }
406 |
407 | except Exception as e:
408 | logger.error(f"Error generating synthesis: {e}")
409 | return {"error": f"Synthesis failed: {e!s}", "status": "error"}
410 |
411 | def get_status() -> dict[str, Any]:
412 | """
413 | Get the current MCTS status and configuration.
414 |
415 | Returns comprehensive information about the current state of the MCTS system
416 | including initialization status, current question, provider settings, and results.
417 |
418 | Returns:
419 | Dict containing all current status information and configuration
420 | """
421 | return {
422 | "initialized": server_state["initialized"],
423 | "current_question": server_state["current_question"],
424 | "chat_id": server_state["chat_id"],
425 | "provider": server_state["provider"],
426 | "model": server_state["model"],
427 | "iterations_completed": server_state["iterations_completed"],
428 | "best_score": server_state["best_score"],
429 | "has_analysis": bool(server_state["best_analysis"]),
430 | "available_providers": ["gemini"],
431 | "api_key_configured": bool(os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"))
432 | }
433 |
434 | def set_provider(provider: str = "gemini", model: str | None = None) -> dict[str, Any]:
435 | """
436 | Set the LLM provider and model configuration.
437 |
438 | Args:
439 | provider: LLM provider name (currently only 'gemini' supported)
440 | model: Specific model name to use (optional)
441 |
442 | Returns:
443 | Dict containing success status and new configuration
444 |
445 | Note:
446 | Currently only supports Gemini provider. Other providers will return an error.
447 | """
448 | if provider.lower() != "gemini":
449 | return {"error": "Only 'gemini' provider is currently supported", "status": "error"}
450 |
451 | server_state["provider"] = provider.lower()
452 | if model:
453 | server_state["model"] = model
454 |
455 | return {
456 | "status": "success",
457 | "provider": server_state["provider"],
458 | "model": server_state["model"],
459 | "message": f"Provider set to {provider}" + (f" with model {model}" if model else "")
460 | }
461 |
462 | def list_available_models(provider: str = "gemini") -> dict[str, Any]:
463 | """
464 | List available models for a given provider.
465 |
466 | Args:
467 | provider: Provider name to list models for (currently only 'gemini' supported)
468 |
469 | Returns:
470 | Dict containing available models, default model, and current configuration
471 |
472 | Note:
473 | Model availability depends on the provider. Currently only Gemini models are supported.
474 | """
475 | if provider.lower() == "gemini":
476 | return {
477 | "provider": "gemini",
478 | "default_model": "gemini-2.0-flash-lite",
479 | "available_models": [
480 | "gemini-2.0-flash-lite",
481 | "gemini-2.0-flash-exp",
482 | "gemini-1.5-pro",
483 | "gemini-1.5-flash"
484 | ],
485 | "current_model": server_state["model"]
486 | }
487 | else:
488 | return {"error": f"Provider '{provider}' not supported", "available_providers": ["gemini"]}
489 |
490 | async def main():
491 | """Main entry point."""
492 | try:
493 | logger.info("Starting Simple MCTS MCP Server... Version: 1.0, Default Provider: Gemini, Default Model: gemini-2.0-flash-lite")
494 | logger.info("Default provider: Gemini")
495 | logger.info("To use: Set GEMINI_API_KEY environment variable")
496 |
497 | # Run server with stdio
498 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
499 | await server.run(
500 | read_stream,
501 | write_stream,
502 | server.create_initialization_options()
503 | )
504 | except KeyboardInterrupt:
505 | logger.info("Server interrupted by user, shutting down...")
506 | except Exception as e:
507 | logger.error(f"Server error: {e}")
508 | sys.exit(1)
509 |
510 | def cli_main() -> None:
511 | """
512 | Synchronous entry point for the CLI script.
513 |
514 | This function is called by the console script entry point in pyproject.toml
515 | and properly runs the async main() function.
516 | """
517 | try:
518 | asyncio.run(main())
519 | except KeyboardInterrupt:
520 | print("\n\nServer shutdown initiated by user")
521 | except Exception as e:
522 | print(f"\n\nServer error: {e}")
523 | sys.exit(1)
524 |
525 | if __name__ == "__main__":
526 | """
527 | Execute the MCTS-MCP-Server.
528 |
529 | ### Execution Flow
530 | 1. Initialize server instance
531 | 2. Configure MCP handlers
532 | 3. Start async event loop
533 | 4. Handle graceful shutdown
534 | """
535 | try:
536 | asyncio.run(main())
537 | except KeyboardInterrupt:
538 | print("\n\nServer shutdown initiated by user")
539 | except Exception as e:
540 | print(f"\n\nServer error: {e}")
541 | sys.exit(1)
542 |
```
--------------------------------------------------------------------------------
/src/mcts_mcp_server/gemini_adapter.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Google Gemini LLM Adapter
3 | =========================
4 |
5 | This module defines the GeminiAdapter class for interacting with Google Gemini models.
6 | Includes rate limiting for free tier models.
7 | """
8 | import logging
9 | import os
10 | from collections.abc import AsyncGenerator
11 | from typing import Any
12 |
13 | from google import genai
14 | from google.genai.types import GenerateContentConfig
15 |
16 | from .base_llm_adapter import BaseLLMAdapter
17 | from .rate_limiter import ModelRateLimitManager, RateLimitConfig
18 |
19 | # Default safety settings for Gemini - can be overridden via kwargs.
20 | # Should be set via .env for accessibility
21 |
22 | DEFAULT_SAFETY_SETTINGS = [
23 | {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
24 | {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
25 | {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
26 | {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
27 | ]
28 |
29 | class GeminiAdapter(BaseLLMAdapter):
30 | """
31 | LLM Adapter for Google Gemini models with rate limiting support.
32 | """
33 | DEFAULT_MODEL = "gemini-2.0-flash-lite"
34 |
35 | def __init__(self, api_key: str | None = None, model_name: str | None = None,
36 | enable_rate_limiting: bool = True, custom_rate_limits: dict[str, RateLimitConfig] | None = None, **kwargs) -> None:
37 | """
38 | Initialize the Gemini LLM adapter with rate limiting support.
39 |
40 | Args:
41 | api_key: Gemini API key (if None, uses GEMINI_API_KEY environment variable)
42 | model_name: Name of the Gemini model to use (defaults to gemini-2.0-flash-lite 30 RPM 1,500 RPD free)
43 | enable_rate_limiting: Whether to enable rate limiting for API calls
44 | custom_rate_limits: Custom rate limit configurations for specific models
45 | **kwargs: Additional arguments passed to BaseLLMAdapter
46 |
47 | Raises:
48 | ValueError: If no API key is provided via argument or environment variable
49 | """
50 | super().__init__(api_key=api_key, **kwargs)
51 |
52 | self.api_key = api_key or os.getenv("GEMINI_API_KEY")
53 | if not self.api_key:
54 | raise ValueError("Gemini API key not provided via argument or GEMINI_API_KEY environment variable.")
55 |
56 | # Configure the client with API key
57 | self.client = genai.Client(api_key=self.api_key)
58 |
59 | self.model_name = model_name or self.DEFAULT_MODEL
60 | self.logger = logging.getLogger(__name__)
61 |
62 | # Initialize rate limiting
63 | self.enable_rate_limiting = enable_rate_limiting
64 | if self.enable_rate_limiting:
65 | # Add specific rate limits for the models mentioned by user
66 | gemini_rate_limits = {
67 | "gemini-2.0-flash-lite": RateLimitConfig(requests_per_minute=30, burst_allowance=2),
68 | "gemini-2.5-flash-preview-05-20": RateLimitConfig(requests_per_minute=10, burst_allowance=1),
69 | "gemini-2.0-flash-exp": RateLimitConfig(requests_per_minute=10, burst_allowance=1),
70 | "gemini-1.5-flash": RateLimitConfig(requests_per_minute=15, burst_allowance=2),
71 | "gemini-1.5-flash-8b": RateLimitConfig(requests_per_minute=15, burst_allowance=2),
72 | "gemini-1.5-pro": RateLimitConfig(requests_per_minute=360, burst_allowance=5),
73 | "gemini-2.0-flash-thinking-exp": RateLimitConfig(requests_per_minute=60, burst_allowance=3),
74 | }
75 |
76 | # Merge with any custom rate limits provided
77 | if custom_rate_limits:
78 | gemini_rate_limits.update(custom_rate_limits)
79 |
80 | self.rate_limit_manager = ModelRateLimitManager(custom_limits=gemini_rate_limits)
81 | self.logger.info(f"Initialized GeminiAdapter with rate limiting enabled for model: {self.model_name}")
82 | else:
83 | self.rate_limit_manager = None
84 | self.logger.info(f"Initialized GeminiAdapter without rate limiting for model: {self.model_name}")
85 |
86 | def _get_model_client(self, model_name_override: str | None = None) -> str:
87 | """
88 | Get the appropriate Gemini model name for the specified model.
89 |
90 | Args:
91 | model_name_override: Optional model name to use instead of instance default
92 |
93 | Returns:
94 | Model name string to use for API calls
95 |
96 | Note:
97 | In the new google-genai library, we use model names directly rather than client objects
98 | """
99 | if model_name_override:
100 | self.logger.debug(f"Using model override: {model_name_override}")
101 | return model_name_override
102 | return self.model_name
103 |
104 | def get_rate_limit_status(self, model_name: str | None = None) -> dict[str, float] | None:
105 | """
106 | Get current rate limit status for a specific model.
107 |
108 | Args:
109 | model_name: Model to check status for (uses instance default if None)
110 |
111 | Returns:
112 | Dictionary containing rate limit status information including:
113 | - requests_remaining: Number of requests available
114 | - time_until_reset: Seconds until rate limit resets
115 | - requests_per_minute: Configured requests per minute limit
116 | Returns None if rate limiting is disabled
117 | """
118 | if not self.enable_rate_limiting or not self.rate_limit_manager:
119 | return None
120 |
121 | target_model = model_name if model_name else self.model_name
122 | limiter = self.rate_limit_manager.get_limiter(target_model)
123 | return limiter.get_status()
124 |
125 | def get_all_rate_limit_status(self) -> dict[str, dict[str, float]] | None:
126 | """
127 | Get rate limit status for all configured models.
128 |
129 | Returns:
130 | Dictionary mapping model names to their rate limit status dictionaries,
131 | or None if rate limiting is disabled
132 |
133 | Note:
134 | Only includes models that have been used or explicitly configured
135 | """
136 | if not self.enable_rate_limiting or not self.rate_limit_manager:
137 | return None
138 |
139 | return self.rate_limit_manager.get_all_status()
140 |
141 | def add_custom_rate_limit(self, model_name: str, requests_per_minute: int, burst_allowance: int = 1) -> None:
142 | """
143 | Add or update a custom rate limit configuration for a specific model.
144 |
145 | Args:
146 | model_name: Name of the Gemini model to configure
147 | requests_per_minute: Maximum requests allowed per minute
148 | burst_allowance: Number of requests that can be made immediately without waiting
149 |
150 | Note:
151 | If rate limiting is disabled, this method logs a warning and does nothing
152 | """
153 | if not self.enable_rate_limiting or not self.rate_limit_manager:
154 | self.logger.warning("Rate limiting is disabled, cannot add custom rate limit")
155 | return
156 |
157 | config = RateLimitConfig(requests_per_minute=requests_per_minute, burst_allowance=burst_allowance)
158 | self.rate_limit_manager.add_custom_limit(model_name, config)
159 | self.logger.info(f"Added custom rate limit for {model_name}: {requests_per_minute} RPM, {burst_allowance} burst")
160 |
161 | def _convert_messages_to_gemini_format(self, messages: list[dict[str, str]]) -> tuple[str | None, list[dict[str, Any]]]:
162 | """
163 | Convert standard message format to Gemini-specific format.
164 |
165 | Args:
166 | messages: List of message dictionaries with 'role' and 'content' keys
167 |
168 | Returns:
169 | Tuple containing:
170 | - system_instruction: Extracted system prompt (if any)
171 | - gemini_messages: Messages formatted for Gemini API with 'parts' structure
172 |
173 | Note:
174 | - Converts 'assistant' role to 'model' for Gemini compatibility
175 | - Extracts system messages as separate system_instruction
176 | - Ignores unsupported roles with warning
177 | """
178 | gemini_messages: list[dict[str, Any]] = []
179 | system_instruction: str | None = None
180 |
181 | if not messages:
182 | return system_instruction, gemini_messages
183 |
184 | current_messages = list(messages)
185 |
186 | # Extract system instruction from first message if it's a system message
187 | if current_messages and current_messages[0].get("role") == "system":
188 | system_instruction = current_messages.pop(0).get("content", "")
189 |
190 | for message in current_messages:
191 | role = message.get("role")
192 | content = message.get("content", "")
193 |
194 | if role == "user":
195 | gemini_messages.append({
196 | 'role': 'user',
197 | 'parts': [{'text': content}]
198 | })
199 | elif role == "assistant":
200 | gemini_messages.append({
201 | 'role': 'model',
202 | 'parts': [{'text': content}]
203 | })
204 | elif role != "system": # System role already handled
205 | self.logger.warning(f"Gemini adapter: Unsupported role '{role}' encountered and skipped.")
206 |
207 | return system_instruction, gemini_messages
208 |
209 | async def get_completion(self, model: str | None, messages: list[dict[str, str]], **kwargs) -> str:
210 | """
211 | Get a non-streaming completion from Gemini with rate limiting.
212 |
213 | Args:
214 | model: Gemini model name to use (uses instance default if None)
215 | messages: Conversation messages in standard format
216 | **kwargs: Additional arguments including:
217 | - generation_config: Gemini generation configuration
218 | - safety_settings: Content safety settings
219 |
220 | Returns:
221 | Generated text response from the model
222 |
223 | Raises:
224 | Exception: If API call fails or rate limiting is violated
225 |
226 | Note:
227 | Automatically applies rate limiting if enabled and handles system instructions
228 | """
229 | effective_model_name = self._get_model_client(model)
230 | target_model_name = model if model else self.model_name
231 |
232 | # Apply rate limiting if enabled
233 | if self.enable_rate_limiting and self.rate_limit_manager:
234 | self.logger.debug(f"Applying rate limit for model: {target_model_name}")
235 | await self.rate_limit_manager.acquire_for_model(target_model_name)
236 |
237 | system_instruction, gemini_messages = self._convert_messages_to_gemini_format(messages)
238 |
239 | if not gemini_messages:
240 | self.logger.warning("No user/model messages to send to Gemini after processing. Returning empty.")
241 | return ""
242 |
243 | # Prepare generation config
244 | generation_config_args = kwargs.get('generation_config', {})
245 | if system_instruction and 'system_instruction' not in generation_config_args:
246 | generation_config_args['system_instruction'] = system_instruction
247 |
248 | # Convert dict to GenerateContentConfig object if not already
249 | if isinstance(generation_config_args, dict):
250 | generation_config = GenerateContentConfig(**generation_config_args)
251 | else:
252 | generation_config = generation_config_args
253 |
254 | # Should be set via .env for accessability
255 | safety_settings = kwargs.get('safety_settings', DEFAULT_SAFETY_SETTINGS)
256 |
257 | self.logger.debug(f"Gemini get_completion using model: {effective_model_name}, messages: {gemini_messages}, system_instruction: {system_instruction}")
258 |
259 | try:
260 | response = await self.client.aio.models.generate_content(
261 | model=effective_model_name,
262 | contents=gemini_messages,
263 | config=generation_config
264 | )
265 |
266 | if response.candidates and len(response.candidates) > 0:
267 | candidate = response.candidates[0]
268 | if candidate.content and candidate.content.parts:
269 | text = candidate.content.parts[0].text
270 | return text if text is not None else ""
271 |
272 | self.logger.warning(f"Gemini response was empty or blocked. Response: {response}")
273 | return "Error: Gemini response empty or blocked."
274 |
275 | except Exception as e:
276 | self.logger.error(f"Gemini API error in get_completion: {e}", exc_info=True)
277 | return f"Error: Gemini API request failed - {type(e).__name__}: {e}"
278 |
279 | async def get_streaming_completion(self, model: str | None, messages: list[dict[str, str]], **kwargs) -> AsyncGenerator[str, None]:
280 | """
281 | Get a streaming completion from Gemini with rate limiting.
282 |
283 | Args:
284 | model: Gemini model name to use (uses instance default if None)
285 | messages: Conversation messages in standard format
286 | **kwargs: Additional arguments including:
287 | - generation_config: Gemini generation configuration
288 | - safety_settings: Content safety settings
289 |
290 | Yields:
291 | Text chunks as they are generated by the model
292 |
293 | Raises:
294 | Exception: If API call fails or rate limiting is violated
295 |
296 | Note:
297 | Applies rate limiting before starting the stream and handles system instructions
298 | """
299 | effective_model_name = self._get_model_client(model)
300 | target_model_name = model if model else self.model_name
301 |
302 | # Apply rate limiting if enabled
303 | if self.enable_rate_limiting and self.rate_limit_manager:
304 | self.logger.debug(f"Applying rate limit for streaming model: {target_model_name}")
305 | await self.rate_limit_manager.acquire_for_model(target_model_name)
306 |
307 | system_instruction, gemini_messages = self._convert_messages_to_gemini_format(messages)
308 |
309 | if not gemini_messages:
310 | self.logger.warning("No user/model messages to send to Gemini for streaming. Yielding nothing.")
311 | return
312 |
313 | generation_config_args = kwargs.get('generation_config', {})
314 | if system_instruction and 'system_instruction' not in generation_config_args:
315 | generation_config_args['system_instruction'] = system_instruction
316 |
317 | if isinstance(generation_config_args, dict):
318 | generation_config = GenerateContentConfig(**generation_config_args)
319 | else:
320 | generation_config = generation_config_args
321 |
322 | self.logger.debug(f"Gemini get_streaming_completion using model: {effective_model_name}, messages: {gemini_messages}")
323 |
324 | try:
325 | response_stream = await self.client.aio.models.generate_content_stream(
326 | model=effective_model_name,
327 | contents=gemini_messages,
328 | config=generation_config
329 | )
330 |
331 | async for chunk in response_stream:
332 | if chunk.candidates and len(chunk.candidates) > 0:
333 | candidate = chunk.candidates[0]
334 | if candidate.content and candidate.content.parts:
335 | for part in candidate.content.parts:
336 | if part.text:
337 | yield part.text
338 |
339 | except Exception as e:
340 | self.logger.error(f"Gemini API error in get_streaming_completion: {e}", exc_info=True)
341 | yield f"Error: Gemini API request failed during stream - {type(e).__name__}: {e}"
342 |
343 | # Example of how to use (for testing purposes)
344 | async def _test_gemini_adapter() -> None:
345 | """
346 | Test function for the GeminiAdapter class.
347 |
348 | Tests various features including:
349 | - Basic completion and streaming
350 | - Rate limiting functionality
351 | - Custom model usage
352 | - Tag generation
353 | - Error handling
354 |
355 | Requires:
356 | GEMINI_API_KEY environment variable to be set
357 |
358 | Note:
359 | This is primarily for development testing and debugging
360 | """
361 | logging.basicConfig(level=logging.INFO)
362 | logger = logging.getLogger(__name__)
363 |
364 | if not os.getenv("GEMINI_API_KEY"):
365 | logger.warning("GEMINI_API_KEY not set, skipping GeminiAdapter direct test.")
366 | return
367 |
368 | try:
369 | # Test with rate limiting enabled (default)
370 | adapter = GeminiAdapter() # Uses default model
371 |
372 | logger.info("Testing rate limit status...")
373 | status = adapter.get_rate_limit_status()
374 | logger.info(f"Rate limit status: {status}")
375 |
376 | # Test adding custom rate limit
377 | adapter.add_custom_rate_limit("gemini-test-model", requests_per_minute=5, burst_allowance=2)
378 |
379 | logger.info("Testing GeminiAdapter get_completion...")
380 | messages = [
381 | {"role": "system", "content": "You are a helpful assistant."},
382 | {"role": "user", "content": "What is the capital of Germany?"}
383 | ]
384 | completion = await adapter.get_completion(model=None, messages=messages)
385 | logger.info(f"Completion result: {completion}")
386 | assert "Berlin" in completion
387 |
388 | logger.info("Testing specific model with rate limiting...")
389 | # Test the specific model mentioned by user
390 | completion_preview = await adapter.get_completion(
391 | model="gemini-2.5-flash-preview-05-20",
392 | messages=[{"role": "user", "content": "Hello, just testing!"}]
393 | )
394 | logger.info(f"Preview model completion: {completion_preview}")
395 |
396 | logger.info("Testing GeminiAdapter get_streaming_completion...")
397 | stream_messages = [{"role": "user", "content": "Write a short fun fact about space."}]
398 | full_streamed_response = ""
399 | async for chunk in adapter.get_streaming_completion(model=None, messages=stream_messages, generation_config={"temperature": 0.7}):
400 | logger.info(f"Stream chunk: '{chunk}'")
401 | full_streamed_response += chunk
402 | logger.info(f"Full streamed response: {full_streamed_response}")
403 | assert len(full_streamed_response) > 0
404 |
405 | logger.info("Testing BaseLLMAdapter generate_tags...")
406 | tags_text = "The quick brown fox jumps over the lazy dog. This is a test for gemini."
407 | tags = await adapter.generate_tags(analysis_text=tags_text, config={}) # Pass empty config
408 | logger.info(f"Generated tags: {tags}")
409 | assert "fox" in tags or "gemini" in tags
410 |
411 | # Test rate limiting status after requests
412 | all_status = adapter.get_all_rate_limit_status()
413 | logger.info(f"All rate limit status after requests: {all_status}")
414 |
415 | # Test adapter without rate limiting
416 | logger.info("Testing adapter without rate limiting...")
417 | no_limit_adapter = GeminiAdapter(enable_rate_limiting=False)
418 | no_limit_status = no_limit_adapter.get_rate_limit_status()
419 | logger.info(f"No rate limit adapter status: {no_limit_status}")
420 |
421 | logger.info("GeminiAdapter tests completed successfully (if API key was present).")
422 |
423 | except ValueError as ve:
424 | logger.error(f"ValueError during GeminiAdapter test (likely API key issue): {ve}")
425 | except Exception as e:
426 | logger.error(f"An unexpected error occurred during GeminiAdapter test: {e}", exc_info=True)
427 |
428 | if __name__ == "__main__":
429 | # To run this test, ensure GEMINI_API_KEY is set
430 | # then run: python -m src.mcts_mcp_server.gemini_adapter
431 | import asyncio
432 | if os.getenv("GEMINI_API_KEY"):
433 | asyncio.run(_test_gemini_adapter())
434 | else:
435 | print("Skipping GeminiAdapter test as GEMINI_API_KEY is not set.")
436 |
```
--------------------------------------------------------------------------------
/archive/tools_original.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Fixed Tools for MCTS with proper async handling
5 | ===============================================
6 |
7 | This module fixes the async event loop issues in the MCTS MCP tools.
8 | """
9 | import asyncio
10 | import json
11 | import logging
12 | import datetime
13 | import os
14 | from dotenv import load_dotenv
15 | from typing import Dict, Any, Optional, List
16 | import concurrent.futures
17 | import threading
18 |
19 | from mcp.server.fastmcp import FastMCP
20 | from .llm_adapter import DirectMcpLLMAdapter
21 |
22 | from .ollama_utils import (
23 | OLLAMA_PYTHON_PACKAGE_AVAILABLE,
24 | check_available_models,
25 | get_recommended_models
26 | )
27 | from .ollama_adapter import OllamaAdapter
28 |
29 | from .mcts_core import MCTS
30 | from .state_manager import StateManager
31 | from .mcts_config import DEFAULT_CONFIG
32 | from .utils import truncate_text
33 |
34 | logger = logging.getLogger(__name__)
35 |
36 | # Global state to maintain between tool calls
37 | _global_state = {
38 | "mcts_instance": None,
39 | "config": None,
40 | "state_manager": None,
41 | "current_chat_id": None,
42 | "active_llm_provider": os.getenv("DEFAULT_LLM_PROVIDER", "ollama"),
43 | "active_model_name": os.getenv("DEFAULT_MODEL_NAME"),
44 | "collect_results": False,
45 | "current_run_id": None,
46 | "ollama_available_models": []
47 | }
48 |
49 | def run_async_safe(coro):
50 | """
51 | Safely run an async coroutine without event loop conflicts.
52 | Uses a dedicated thread pool executor to avoid conflicts.
53 | """
54 | def sync_runner():
55 | # Create a new event loop for this thread
56 | loop = asyncio.new_event_loop()
57 | asyncio.set_event_loop(loop)
58 | try:
59 | return loop.run_until_complete(coro)
60 | finally:
61 | loop.close()
62 |
63 | # Use a thread pool executor to run the coroutine
64 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
65 | future = executor.submit(sync_runner)
66 | return future.result()
67 |
68 | def register_mcts_tools(mcp: FastMCP, db_path: str):
69 | """
70 | Register all MCTS-related tools with the MCP server.
71 | """
72 | global _global_state
73 |
74 | # Load environment variables
75 | load_dotenv()
76 |
77 | # Initialize state manager
78 | _global_state["state_manager"] = StateManager(db_path)
79 |
80 | # Initialize config
81 | _global_state["config"] = DEFAULT_CONFIG.copy()
82 |
83 | # Populate available models
84 | _global_state["ollama_available_models"] = check_available_models()
85 | if not _global_state["ollama_available_models"]:
86 | logger.warning("No Ollama models detected.")
87 |
88 | # Set default model for ollama if needed
89 | if _global_state["active_llm_provider"] == "ollama" and not _global_state["active_model_name"]:
90 | _global_state["active_model_name"] = OllamaAdapter.DEFAULT_MODEL
91 |
92 | @mcp.tool()
93 | def initialize_mcts(question: str, chat_id: str, provider_name: Optional[str] = None,
94 | model_name: Optional[str] = None, config_updates: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
95 | """Initialize the MCTS system with proper async handling."""
96 | global _global_state
97 |
98 | try:
99 | logger.info(f"Initializing MCTS for chat ID: {chat_id}")
100 |
101 | # Determine target provider and model
102 | target_provider = provider_name or _global_state["active_llm_provider"]
103 | target_model = model_name or _global_state["active_model_name"]
104 |
105 | logger.info(f"Using LLM Provider: {target_provider}, Model: {target_model}")
106 |
107 | # Update config if provided
108 | if config_updates:
109 | cfg = _global_state["config"].copy()
110 | cfg.update(config_updates)
111 | _global_state["config"] = cfg
112 | else:
113 | cfg = _global_state["config"]
114 |
115 | _global_state["current_chat_id"] = chat_id
116 | state_manager = _global_state["state_manager"]
117 | loaded_state = state_manager.load_state(chat_id) if cfg.get("enable_state_persistence", True) else None
118 |
119 | # Instantiate the appropriate adapter
120 | llm_adapter = None
121 | if target_provider == "ollama":
122 | if not target_model:
123 | target_model = OllamaAdapter.DEFAULT_MODEL
124 | if target_model not in _global_state["ollama_available_models"]:
125 | return {
126 | "status": "model_error",
127 | "error": f"Ollama model '{target_model}' not available",
128 | "available_models": _global_state["ollama_available_models"]
129 | }
130 | llm_adapter = OllamaAdapter(model_name=target_model)
131 |
132 | elif target_provider == "openai":
133 | from .openai_adapter import OpenAIAdapter
134 | if not target_model:
135 | target_model = OpenAIAdapter.DEFAULT_MODEL
136 | llm_adapter = OpenAIAdapter(api_key=os.getenv("OPENAI_API_KEY"), model_name=target_model)
137 |
138 | elif target_provider == "anthropic":
139 | from .anthropic_adapter import AnthropicAdapter
140 | if not target_model:
141 | target_model = AnthropicAdapter.DEFAULT_MODEL
142 | llm_adapter = AnthropicAdapter(api_key=os.getenv("ANTHROPIC_API_KEY"), model_name=target_model)
143 |
144 | elif target_provider == "gemini":
145 | from .gemini_adapter import GeminiAdapter
146 | if not target_model:
147 | target_model = GeminiAdapter.DEFAULT_MODEL
148 | llm_adapter = GeminiAdapter(api_key=os.getenv("GEMINI_API_KEY"), model_name=target_model)
149 |
150 | else:
151 | return {"error": f"Unsupported LLM provider: {target_provider}", "status": "error"}
152 |
153 | _global_state["active_llm_provider"] = target_provider
154 | _global_state["active_model_name"] = target_model
155 |
156 | # Generate initial analysis using the safe async runner
157 | async def generate_initial():
158 | initial_prompt = f"<instruction>Provide an initial analysis of the following question. Be clear and concise.</instruction><question>{question}</question>"
159 | initial_messages = [{"role": "user", "content": initial_prompt}]
160 | return await llm_adapter.get_completion(model=target_model, messages=initial_messages)
161 |
162 | try:
163 | initial_analysis = run_async_safe(generate_initial())
164 | except Exception as e:
165 | logger.error(f"Failed to generate initial analysis: {e}")
166 | return {"error": f"Failed to generate initial analysis: {str(e)}", "status": "error"}
167 |
168 | # Create MCTS instance
169 | _global_state["mcts_instance"] = MCTS(
170 | llm_interface=llm_adapter,
171 | question=question,
172 | initial_analysis_content=initial_analysis or "No initial analysis available",
173 | config=cfg,
174 | initial_state=loaded_state
175 | )
176 |
177 | return {
178 | "status": "initialized",
179 | "question": question,
180 | "chat_id": chat_id,
181 | "initial_analysis": initial_analysis,
182 | "loaded_state": loaded_state is not None,
183 | "provider": target_provider,
184 | "model_used": target_model,
185 | "config": {k: v for k, v in cfg.items() if not k.startswith("_")},
186 | "run_id": _global_state.get("current_run_id")
187 | }
188 |
189 | except ValueError as ve:
190 | logger.error(f"Configuration error: {ve}")
191 | return {"error": f"Configuration error: {str(ve)}", "status": "config_error"}
192 | except Exception as e:
193 | logger.error(f"Error in initialize_mcts: {e}")
194 | return {"error": f"Failed to initialize MCTS: {str(e)}", "status": "error"}
195 |
196 | @mcp.tool()
197 | def set_active_llm(provider_name: str, model_name: Optional[str] = None) -> Dict[str, Any]:
198 | """Set the active LLM provider and model."""
199 | global _global_state
200 | supported_providers = ["ollama", "openai", "anthropic", "gemini"]
201 | provider_name_lower = provider_name.lower()
202 |
203 | if provider_name_lower not in supported_providers:
204 | return {
205 | "status": "error",
206 | "message": f"Unsupported provider: '{provider_name}'. Supported: {supported_providers}"
207 | }
208 |
209 | _global_state["active_llm_provider"] = provider_name_lower
210 | _global_state["active_model_name"] = model_name
211 |
212 | log_msg = f"Set active LLM provider to: {provider_name_lower}."
213 | if model_name:
214 | log_msg += f" Set active model to: {model_name}."
215 |
216 | return {"status": "success", "message": log_msg}
217 |
218 | @mcp.tool()
219 | def list_ollama_models() -> Dict[str, Any]:
220 | """List all available Ollama models."""
221 | logger.info("Listing Ollama models...")
222 |
223 | # Check if Ollama server is running
224 | try:
225 | import httpx
226 | with httpx.Client(base_url="http://localhost:11434", timeout=3.0) as client:
227 | response = client.get("/")
228 | if response.status_code != 200:
229 | return {
230 | "status": "error",
231 | "message": "Ollama server not responding. Please ensure Ollama is running."
232 | }
233 | except Exception as e:
234 | return {
235 | "status": "error",
236 | "message": f"Cannot connect to Ollama server: {str(e)}"
237 | }
238 |
239 | # Get available models
240 | available_models = check_available_models()
241 | if not available_models:
242 | return {
243 | "status": "error",
244 | "message": "No Ollama models found. Try 'ollama pull MODEL_NAME' to download a model."
245 | }
246 |
247 | # Get recommendations
248 | recommendations = get_recommended_models(available_models)
249 | current_model = _global_state.get("active_model_name") if _global_state.get("active_llm_provider") == "ollama" else None
250 |
251 | # Update global state
252 | _global_state["ollama_available_models"] = available_models
253 |
254 | return {
255 | "status": "success",
256 | "ollama_available_models": available_models,
257 | "current_ollama_model": current_model,
258 | "recommended_small_models": recommendations["small_models"],
259 | "recommended_medium_models": recommendations["medium_models"],
260 | "message": f"Found {len(available_models)} Ollama models"
261 | }
262 |
263 | @mcp.tool()
264 | def run_mcts(iterations: int = 1, simulations_per_iteration: int = 5, model_name: Optional[str] = None) -> Dict[str, Any]:
265 | """Run the MCTS algorithm with proper async handling."""
266 | global _global_state
267 |
268 | mcts = _global_state.get("mcts_instance")
269 | if not mcts:
270 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
271 |
272 | active_provider = _global_state.get("active_llm_provider")
273 | active_model = _global_state.get("active_model_name")
274 |
275 | if not active_provider or not active_model:
276 | return {"error": "Active LLM provider or model not set."}
277 |
278 | # Update config for this run
279 | temp_config = mcts.config.copy()
280 | temp_config["max_iterations"] = iterations
281 | temp_config["simulations_per_iteration"] = simulations_per_iteration
282 | mcts.config = temp_config
283 |
284 | logger.info(f"Starting MCTS run with {iterations} iterations, {simulations_per_iteration} simulations per iteration")
285 |
286 | def run_mcts_background():
287 | """Run MCTS in background thread with proper async handling."""
288 | try:
289 | # Use the safe async runner
290 | async def run_search():
291 | await mcts.run_search_iterations(iterations, simulations_per_iteration)
292 | return mcts.get_final_results()
293 |
294 | results = run_async_safe(run_search())
295 |
296 | # Save state if enabled
297 | if temp_config.get("enable_state_persistence", True) and _global_state["current_chat_id"]:
298 | try:
299 | _global_state["state_manager"].save_state(_global_state["current_chat_id"], mcts)
300 | logger.info(f"Saved state for chat ID: {_global_state['current_chat_id']}")
301 | except Exception as e:
302 | logger.error(f"Error saving state: {e}")
303 |
304 | # Get best node and tags
305 | best_node = mcts.find_best_final_node()
306 | tags = best_node.descriptive_tags if best_node else []
307 |
308 | logger.info(f"MCTS run completed. Best score: {results.best_score if results else 0.0}")
309 |
310 | except Exception as e:
311 | logger.error(f"Error in background MCTS run: {e}")
312 |
313 | # Start background thread
314 | background_thread = threading.Thread(target=run_mcts_background)
315 | background_thread.daemon = True
316 | background_thread.start()
317 |
318 | return {
319 | "status": "started",
320 | "message": f"MCTS process started with {iterations} iterations and {simulations_per_iteration} simulations per iteration.",
321 | "provider": active_provider,
322 | "model": active_model,
323 | "background_thread_id": background_thread.ident
324 | }
325 |
326 | @mcp.tool()
327 | def generate_synthesis() -> Dict[str, Any]:
328 | """Generate a final synthesis of the MCTS results."""
329 | global _global_state
330 |
331 | mcts = _global_state.get("mcts_instance")
332 | if not mcts:
333 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
334 |
335 | try:
336 | async def synth():
337 | llm_adapter = mcts.llm
338 | path_nodes = mcts.get_best_path_nodes()
339 |
340 | path_thoughts_list = [
341 | f"- (Node {node.sequence}): {node.thought.strip()}"
342 | for node in path_nodes if node.thought and node.parent
343 | ]
344 | path_thoughts_str = "\n".join(path_thoughts_list) if path_thoughts_list else "No significant development path identified."
345 |
346 | results = mcts.get_final_results()
347 |
348 | synth_context = {
349 | "question_summary": mcts.question_summary,
350 | "initial_analysis_summary": truncate_text(mcts.root.content, 300) if mcts.root else "N/A",
351 | "best_score": f"{results.best_score:.1f}",
352 | "path_thoughts": path_thoughts_str,
353 | "final_best_analysis_summary": truncate_text(results.best_solution_content, 400),
354 | "previous_best_summary": "N/A",
355 | "unfit_markers_summary": "N/A",
356 | "learned_approach_summary": "N/A"
357 | }
358 |
359 | synthesis = await llm_adapter.synthesize_result(synth_context, mcts.config)
360 | best_node = mcts.find_best_final_node()
361 | tags = best_node.descriptive_tags if best_node else []
362 |
363 | return {
364 | "synthesis": synthesis,
365 | "best_score": results.best_score,
366 | "tags": tags,
367 | "iterations_completed": mcts.iterations_completed,
368 | "provider": _global_state.get("active_llm_provider"),
369 | "model": _global_state.get("active_model_name"),
370 | }
371 |
372 | synthesis_result = run_async_safe(synth())
373 | return synthesis_result
374 |
375 | except Exception as e:
376 | logger.error(f"Error generating synthesis: {e}")
377 | return {"error": f"Synthesis generation failed: {str(e)}"}
378 |
379 | @mcp.tool()
380 | def get_config() -> Dict[str, Any]:
381 | """Get the current MCTS configuration."""
382 | global _global_state
383 | config = {k: v for k, v in _global_state["config"].items() if not k.startswith("_")}
384 | config.update({
385 | "active_llm_provider": _global_state.get("active_llm_provider"),
386 | "active_model_name": _global_state.get("active_model_name"),
387 | "ollama_python_package_available": OLLAMA_PYTHON_PACKAGE_AVAILABLE,
388 | "ollama_available_models": _global_state.get("ollama_available_models", []),
389 | "current_run_id": _global_state.get("current_run_id")
390 | })
391 | return config
392 |
393 | @mcp.tool()
394 | def update_config(config_updates: Dict[str, Any]) -> Dict[str, Any]:
395 | """Update the MCTS configuration."""
396 | global _global_state
397 |
398 | logger.info(f"Updating MCTS config with: {config_updates}")
399 |
400 | # Provider and model changes should use set_active_llm
401 | if "active_llm_provider" in config_updates or "active_model_name" in config_updates:
402 | logger.warning("Use 'set_active_llm' tool to change LLM provider or model.")
403 | config_updates.pop("active_llm_provider", None)
404 | config_updates.pop("active_model_name", None)
405 |
406 | # Update config
407 | cfg = _global_state["config"].copy()
408 | cfg.update(config_updates)
409 | _global_state["config"] = cfg
410 |
411 | mcts = _global_state.get("mcts_instance")
412 | if mcts:
413 | mcts.config = cfg
414 |
415 | return get_config()
416 |
417 | @mcp.tool()
418 | def get_mcts_status() -> Dict[str, Any]:
419 | """Get the current status of the MCTS system."""
420 | global _global_state
421 |
422 | mcts = _global_state.get("mcts_instance")
423 | if not mcts:
424 | return {
425 | "initialized": False,
426 | "message": "MCTS not initialized. Call initialize_mcts first."
427 | }
428 |
429 | try:
430 | best_node = mcts.find_best_final_node()
431 | tags = best_node.descriptive_tags if best_node else []
432 |
433 | return {
434 | "initialized": True,
435 | "chat_id": _global_state.get("current_chat_id"),
436 | "iterations_completed": getattr(mcts, "iterations_completed", 0),
437 | "simulations_completed": getattr(mcts, "simulations_completed", 0),
438 | "best_score": getattr(mcts, "best_score", 0.0),
439 | "best_content_summary": truncate_text(getattr(mcts, "best_solution", ""), 100),
440 | "tags": tags,
441 | "tree_depth": mcts.memory.get("depth", 0) if hasattr(mcts, "memory") else 0,
442 | "approach_types": getattr(mcts, "approach_types", []),
443 | "active_llm_provider": _global_state.get("active_llm_provider"),
444 | "active_model_name": _global_state.get("active_model_name"),
445 | "run_id": _global_state.get("current_run_id")
446 | }
447 | except Exception as e:
448 | logger.error(f"Error getting MCTS status: {e}")
449 | return {
450 | "initialized": True,
451 | "error": f"Error getting MCTS status: {str(e)}",
452 | "chat_id": _global_state.get("current_chat_id")
453 | }
454 |
455 | @mcp.tool()
456 | def run_model_comparison(question: str, iterations: int = 2, simulations_per_iteration: int = 10) -> Dict[str, Any]:
457 | """Run MCTS across multiple models for comparison."""
458 | if not OLLAMA_PYTHON_PACKAGE_AVAILABLE:
459 | return {"error": "Ollama python package not available for model comparison."}
460 |
461 | # Get available models
462 | models = check_available_models()
463 | recommendations = get_recommended_models(models)
464 | comparison_models = recommendations["small_models"]
465 |
466 | if not comparison_models:
467 | return {"error": f"No suitable models found for comparison. Available: {models}"}
468 |
469 | return {
470 | "status": "started",
471 | "message": "Model comparison feature available but not implemented in this version",
472 | "question": question,
473 | "models": comparison_models,
474 | "iterations": iterations,
475 | "simulations_per_iteration": simulations_per_iteration
476 | }
477 |
```
--------------------------------------------------------------------------------
/src/mcts_mcp_server/llm_adapter.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | MCP Server to MCTS LLM Adapter
5 | ==============================
6 |
7 | This module adapts the MCP server's LLM access to the LLMInterface
8 | required by the MCTS implementation.
9 | """
10 | import asyncio
11 | import re
12 | import logging
13 | from typing import List, Dict, Any, AsyncGenerator # Protocol removed as LLMInterface is imported
14 | from .llm_interface import LLMInterface # Import the official LLMInterface
15 |
16 | logger = logging.getLogger("llm_adapter")
17 |
18 |
19 | # LLMInterface protocol definition removed from here
20 |
21 | class LocalInferenceLLMAdapter(LLMInterface):
22 | """
23 | LLM adapter that uses simple deterministic rules for generating responses.
24 | This adapter doesn't call external LLMs but performs simplified inference locally.
25 | """
26 |
27 | def __init__(self, mcp_server=None):
28 | """
29 | Initialize the adapter.
30 |
31 | Args:
32 | mcp_server: Optional MCP server instance (not used directly)
33 | """
34 | self.mcp_server = mcp_server
35 | logger.info("Initialized LocalInferenceLLMAdapter")
36 |
37 | async def get_completion(self, model: str, messages: List[Dict[str, str]], **kwargs) -> str:
38 | """Gets a sophisticated completion based on the input messages."""
39 | try:
40 | # Extract the user's message content (usually the last message)
41 | user_content = ""
42 | for msg in reversed(messages):
43 | if msg.get("role") == "user" and msg.get("content"):
44 | user_content = msg["content"]
45 | break
46 |
47 | if not user_content:
48 | return "No input detected."
49 |
50 | # For very structured internal prompts, provide focused responses
51 | # But make them more intelligent and contextual
52 | if "<instruction>" in user_content and any(marker in user_content for marker in [
53 | "Critically examine", "Substantially revise", "Evaluate the intellectual quality",
54 | "Generate concise keyword tags", "Synthesize the key insights", "Classify user requests"
55 | ]):
56 | # Extract key information from the structured prompt
57 | if "**Question being analyzed:**" in user_content:
58 | # Extract the actual question being analyzed
59 | question_match = re.search(r'\*\*Question being analyzed:\*\* (.+)', user_content)
60 | question = question_match.group(1) if question_match else "the topic"
61 |
62 | if "Critically examine" in user_content:
63 | return f"The analysis of '{question}' would benefit from examining this through a systems thinking lens - how do the various components interact dynamically, and what emergent properties might we be missing?"
64 | elif "Substantially revise" in user_content:
65 | return f"To strengthen this analysis of '{question}', we should integrate multiple theoretical frameworks and examine the underlying assumptions more rigorously, particularly considering how context shapes our interpretation."
66 | elif "Synthesize the key insights" in user_content:
67 | return f"The exploration of '{question}' reveals that robust understanding emerges through systematic examination of interconnected perspectives, highlighting the importance of both analytical depth and synthetic integration."
68 |
69 | # Fallback responses for other structured prompts
70 | if "Evaluate the intellectual quality" in user_content:
71 | return "7"
72 | elif "Generate concise keyword tags" in user_content:
73 | return "systems-thinking, analytical-framework, critical-analysis, perspective-integration, contextual-understanding"
74 | elif "Classify user requests" in user_content:
75 | return "ANALYZE_NEW"
76 | else:
77 | return "I've analyzed the structured input and generated a contextually appropriate response that addresses the specific analytical requirements."
78 |
79 | # For natural conversation and general inputs, be more conversational and adaptive
80 | # This allows for fluid interaction while still being helpful
81 | return "I'm here to help you think through complex topics using systematic analytical approaches. What would you like to explore together?"
82 |
83 | except Exception as e:
84 | logger.error(f"Error in get_completion: {e}")
85 | return f"Error: {str(e)}"
86 |
87 | async def get_streaming_completion(self, model: str, messages: List[Dict[str, str]], **kwargs) -> AsyncGenerator[str, None]:
88 | """Gets a streaming completion by breaking up a regular completion."""
89 | try:
90 | full_response = await self.get_completion(model, messages, **kwargs)
91 | chunks = [full_response[i:i+20] for i in range(0, len(full_response), 20)]
92 |
93 | for chunk in chunks:
94 | yield chunk
95 | await asyncio.sleep(0.01)
96 | except Exception as e:
97 | logger.error(f"Error in get_streaming_completion: {e}")
98 | yield f"Error: {str(e)}"
99 |
100 | async def generate_thought(self, context: Dict[str, Any], config: Dict[str, Any]) -> str:
101 | """Generates a critical thought or new direction based on context."""
102 | try:
103 | # Extract context information
104 | question_summary = context.get("question_summary", "")
105 | current_approach = context.get("current_approach", "initial")
106 | current_analysis = context.get("answer", "")
107 | iteration_count = context.get("iteration", 0)
108 |
109 | # Build a sophisticated prompt for the LLM
110 | messages = [
111 | {
112 | "role": "system",
113 | "content": "You are an expert analytical thinker skilled at generating critical insights and new analytical directions. Your role is to examine existing analysis and suggest a specific, actionable critique or new perspective that will meaningfully advance understanding."
114 | },
115 | {
116 | "role": "user",
117 | "content": f"""<instruction>Critically examine the current analytical approach and generate a specific, actionable critique or new direction.
118 |
119 | **Question being analyzed:** {question_summary}
120 |
121 | **Current analytical approach:** {current_approach}
122 |
123 | **Current analysis (if any):** {current_analysis[:500] if current_analysis else "No analysis yet - this is the initial exploration."}
124 |
125 | **Iteration:** {iteration_count + 1}
126 |
127 | Your task is to:
128 | 1. Identify a specific weakness, gap, or limitation in the current approach/analysis
129 | 2. Suggest a concrete new direction, framework, or perspective that would address this limitation
130 | 3. Be specific about what should be examined differently
131 |
132 | Generate your critique as a single, focused suggestion (1-2 sentences) that provides clear direction for improving the analysis. Avoid generic advice - be specific to this particular question and current state of analysis.</instruction>"""
133 | }
134 | ]
135 |
136 | # Use the LLM to generate a thoughtful response
137 | response = await self.get_completion("default", messages)
138 | return response.strip()
139 |
140 | except Exception as e:
141 | logger.error(f"Error in generate_thought: {e}")
142 | # Fallback to a simple contextual thought
143 | question_summary = context.get("question_summary", "the topic")
144 | return f"Consider examining '{question_summary}' from an unexplored angle - what fundamental assumptions might we be overlooking?"
145 |
146 | async def update_analysis(self, critique: str, context: Dict[str, Any], config: Dict[str, Any]) -> str:
147 | """Revises analysis based on critique and context."""
148 | try:
149 | # Extract context information
150 | question_summary = context.get("question_summary", "")
151 | current_approach = context.get("current_approach", "initial")
152 | original_content = context.get("answer", "")
153 | iteration_count = context.get("iteration", 0)
154 |
155 | # Build a sophisticated prompt for the LLM
156 | messages = [
157 | {
158 | "role": "system",
159 | "content": "You are an expert analytical thinker skilled at revising and improving analysis based on critical feedback. Your role is to substantially enhance existing analysis by incorporating specific critiques and suggestions."
160 | },
161 | {
162 | "role": "user",
163 | "content": f"""<instruction>Substantially revise and improve the following analysis by incorporating the provided critique.
164 |
165 | **Original Question:** {question_summary}
166 |
167 | **Current Analytical Approach:** {current_approach}
168 |
169 | **Original Analysis:**
170 | {original_content}
171 |
172 | **Critique to Incorporate:**
173 | <critique>{critique}</critique>
174 |
175 | **Iteration:** {iteration_count + 1}
176 |
177 | Your task is to:
178 | 1. Carefully consider how the critique identifies weaknesses or gaps in the original analysis
179 | 2. Substantially revise the analysis to address these concerns
180 | 3. Integrate new perspectives, frameworks, or evidence as suggested by the critique
181 | 4. Produce a more sophisticated, nuanced analysis that builds meaningfully on the original
182 |
183 | Provide a completely rewritten analysis that demonstrates clear improvement over the original. The revision should be substantive, not superficial - show genuine analytical advancement.</instruction>"""
184 | }
185 | ]
186 |
187 | # Use the LLM to generate a thoughtful response
188 | response = await self.get_completion("default", messages)
189 | return response.strip()
190 |
191 | except Exception as e:
192 | logger.error(f"Error in update_analysis: {e}")
193 | return f"Error: {str(e)}"
194 |
195 | async def evaluate_analysis(self, analysis_to_evaluate: str, context: Dict[str, Any], config: Dict[str, Any]) -> int:
196 | """Evaluates analysis quality (1-10 score)."""
197 | try:
198 | # Extract context information
199 | question_summary = context.get("question_summary", "")
200 | current_approach = context.get("current_approach", "initial")
201 | iteration_count = context.get("iteration", 0)
202 |
203 | # Build a sophisticated prompt for the LLM
204 | messages = [
205 | {
206 | "role": "system",
207 | "content": "You are an expert analytical evaluator skilled at assessing the intellectual quality and depth of analysis. Your role is to provide objective, rigorous evaluation of analytical work on a 1-10 scale."
208 | },
209 | {
210 | "role": "user",
211 | "content": f"""<instruction>Evaluate the intellectual quality of the following analysis on a scale of 1-10.
212 |
213 | **Original Question:** {question_summary}
214 |
215 | **Analytical Approach:** {current_approach}
216 |
217 | **Analysis to Evaluate:**
218 | {analysis_to_evaluate}
219 |
220 | **Iteration:** {iteration_count + 1}
221 |
222 | Evaluation Criteria (1-10 scale):
223 | - 1-3: Superficial, generic, or factually incorrect
224 | - 4-5: Basic understanding but lacks depth or insight
225 | - 6-7: Solid analysis with some meaningful insights
226 | - 8-9: Sophisticated, nuanced analysis with strong insights
227 | - 10: Exceptional depth, originality, and comprehensive understanding
228 |
229 | Consider:
230 | 1. Depth of insight and analytical sophistication
231 | 2. Relevance and specificity to the question
232 | 3. Use of evidence, examples, or frameworks
233 | 4. Logical coherence and structure
234 | 5. Originality of perspective or approach
235 | 6. Practical applicability of insights
236 |
237 | Respond with only a single number (1-10) representing your evaluation score.</instruction>"""
238 | }
239 | ]
240 |
241 | # Use the LLM to generate a thoughtful response
242 | response = await self.get_completion("default", messages)
243 |
244 | # Extract the numeric score from the response
245 | try:
246 | score = int(response.strip())
247 | return max(1, min(10, score)) # Ensure score is in valid range
248 | except ValueError:
249 | # Fallback: try to extract first number from response
250 | import re
251 | numbers = re.findall(r'\d+', response)
252 | if numbers:
253 | score = int(numbers[0])
254 | return max(1, min(10, score))
255 | else:
256 | logger.warning(f"Could not parse score from response: '{response}'. Defaulting to 5.")
257 | return 5
258 |
259 | except Exception as e:
260 | logger.warning(f"Error in evaluate_analysis: '{e}'. Defaulting to 5.")
261 | return 5
262 |
263 | async def generate_tags(self, analysis_text: str, config: Dict[str, Any]) -> List[str]:
264 | """Generates keyword tags for the analysis."""
265 | try:
266 | # Build a sophisticated prompt for the LLM
267 | messages = [
268 | {
269 | "role": "system",
270 | "content": "You are an expert at generating precise, meaningful keyword tags that capture the essential concepts, themes, and analytical approaches in text. Your tags should be specific, insightful, and useful for categorization and discovery."
271 | },
272 | {
273 | "role": "user",
274 | "content": f"""Generate concise keyword tags for the following analysis text.
275 |
276 | **Analysis Text:**
277 | {analysis_text[:1000]} # Limit to avoid token limits
278 |
279 | Your task is to:
280 | 1. Identify the key concepts, themes, and analytical approaches
281 | 2. Generate 3-5 specific, meaningful tags
282 | 3. Focus on substantive content rather than generic terms
283 | 4. Use single words or short phrases (2-3 words max)
284 | 5. Prioritize tags that would help categorize or find this analysis
285 |
286 | Respond with only the tags, separated by commas (e.g., "cognitive-bias, decision-theory, behavioral-economics, systematic-analysis, framework-comparison")."""
287 | }
288 | ]
289 |
290 | # Use the LLM to generate tags
291 | response = await self.get_completion("default", messages)
292 |
293 | # Parse the response into a list of tags
294 | tags = [tag.strip().lower() for tag in response.split(',')]
295 | tags = [tag for tag in tags if tag and len(tag) > 2] # Filter out empty or very short tags
296 |
297 | return tags[:5] # Return up to 5 tags
298 |
299 | except Exception as e:
300 | logger.error(f"Error in generate_tags: {e}")
301 | # Fallback to a simple extraction if LLM fails
302 | words = analysis_text.lower().split()
303 | common_words = {"the", "and", "is", "in", "to", "of", "a", "for", "this", "that", "with", "be", "as", "can", "will", "would", "should"}
304 | filtered_words = [word for word in words if word not in common_words and len(word) > 4]
305 | return list(set(filtered_words[:3])) # Return unique words as fallback
306 |
307 | async def synthesize_result(self, context: Dict[str, Any], config: Dict[str, Any]) -> str:
308 | """Generates a final synthesis based on the MCTS results."""
309 | try:
310 | # Extract context information
311 | question_summary = context.get("question_summary", "")
312 | best_score = context.get("best_score", "0")
313 | final_analysis = context.get("final_best_analysis_summary", "")
314 | all_approaches = context.get("all_approaches", [])
315 | total_iterations = context.get("total_iterations", 0)
316 |
317 | # Build a sophisticated prompt for the LLM
318 | messages = [
319 | {
320 | "role": "system",
321 | "content": "You are an expert analytical synthesizer skilled at drawing together insights from multiple analytical approaches to create comprehensive, nuanced conclusions. Your role is to synthesize the best insights from an iterative analysis process."
322 | },
323 | {
324 | "role": "user",
325 | "content": f"""<instruction>Synthesize the key insights from this multi-approach analytical exploration into a comprehensive conclusion.
326 |
327 | **Original Question:** {question_summary}
328 |
329 | **Best Analysis Found (Score: {best_score}/10):**
330 | {final_analysis}
331 |
332 | **Analytical Approaches Explored:** {', '.join(all_approaches) if all_approaches else 'Multiple iterative approaches'}
333 |
334 | **Total Iterations:** {total_iterations}
335 |
336 | **Final Confidence Score:** {best_score}/10
337 |
338 | Your task is to:
339 | 1. Synthesize the most valuable insights from the analytical exploration
340 | 2. Identify the key patterns, connections, or principles that emerged
341 | 3. Articulate what makes the final analysis particularly compelling
342 | 4. Reflect on how the iterative process enhanced understanding
343 | 5. Provide a nuanced, comprehensive conclusion that captures the depth achieved
344 |
345 | Generate a thoughtful synthesis that demonstrates the analytical journey's value and presents the most robust conclusions reached. Focus on substance and insight rather than process description.</instruction>"""
346 | }
347 | ]
348 |
349 | # Use the LLM to generate a thoughtful response
350 | response = await self.get_completion("default", messages)
351 | return response.strip()
352 |
353 | except Exception as e:
354 | logger.error(f"Error in synthesize_result: {e}")
355 | return f"Error synthesizing result: {str(e)}"
356 |
357 | async def classify_intent(self, text_to_classify: str, config: Dict[str, Any]) -> str:
358 | """Classifies user intent using the LLM."""
359 | try:
360 | # Build a sophisticated prompt for the LLM
361 | messages = [
362 | {
363 | "role": "system",
364 | "content": "You are an expert at understanding user intent and classifying requests into specific categories. Your role is to analyze user input and determine their primary intent from a predefined set of categories."
365 | },
366 | {
367 | "role": "user",
368 | "content": f"""Classify the following user request into one of these specific intent categories:
369 |
370 | **User Input:** "{text_to_classify}"
371 |
372 | **Intent Categories:**
373 | - ANALYZE_NEW: User wants to start a new analysis of a topic, question, or problem
374 | - CONTINUE_ANALYSIS: User wants to continue, expand, or elaborate on an existing analysis
375 | - ASK_LAST_RUN_SUMMARY: User wants to see results, summary, or score from the most recent analysis
376 | - ASK_PROCESS: User wants to understand how the system works, the algorithm, or methodology
377 | - ASK_CONFIG: User wants to know about configuration, settings, or parameters
378 | - GENERAL_CONVERSATION: User is making casual conversation or asking unrelated questions
379 |
380 | Consider the context, tone, and specific language used. Look for:
381 | - New analysis requests: "analyze", "examine", "what do you think about", "explore"
382 | - Continuation requests: "continue", "more", "elaborate", "expand on", "keep going"
383 | - Summary requests: "what did you find", "results", "summary", "score"
384 | - Process questions: "how do you work", "what's your method", "algorithm"
385 | - Config questions: "settings", "parameters", "configuration"
386 |
387 | Respond with only the category name (e.g., "ANALYZE_NEW")."""
388 | }
389 | ]
390 |
391 | # Use the LLM to classify intent
392 | response = await self.get_completion("default", messages)
393 |
394 | # Clean and validate the response
395 | intent = response.strip().upper()
396 | valid_intents = ["ANALYZE_NEW", "CONTINUE_ANALYSIS", "ASK_LAST_RUN_SUMMARY",
397 | "ASK_PROCESS", "ASK_CONFIG", "GENERAL_CONVERSATION"]
398 |
399 | if intent in valid_intents:
400 | return intent
401 | else:
402 | logger.warning(f"LLM returned invalid intent: '{response}'. Defaulting to ANALYZE_NEW.")
403 | return "ANALYZE_NEW"
404 |
405 | except Exception as e:
406 | logger.error(f"Error in classify_intent: {e}")
407 | return "ANALYZE_NEW" # Default on error
408 |
409 | # For backward compatibility, alias the class
410 | McpLLMAdapter = LocalInferenceLLMAdapter
411 | DirectMcpLLMAdapter = LocalInferenceLLMAdapter
412 |
```
--------------------------------------------------------------------------------
/archive/tools_fast.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Optimized Tools for MCTS with deferred initialization
5 | ====================================================
6 |
7 | This module provides fast MCP server startup by deferring heavy
8 | operations until they're actually needed.
9 | """
10 | import asyncio
11 | import logging
12 | import os
13 | import sys
14 | from typing import Dict, Any, Optional
15 | import threading
16 |
17 | from mcp.server.fastmcp import FastMCP
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 | # Global state to maintain between tool calls
22 | _global_state = {
23 | "mcts_instance": None,
24 | "config": None,
25 | "state_manager": None,
26 | "current_chat_id": None,
27 | "active_llm_provider": None,
28 | "active_model_name": None,
29 | "collect_results": False,
30 | "current_run_id": None,
31 | "ollama_available_models": [],
32 | "background_loop": None,
33 | "background_thread": None,
34 | "initialized": False
35 | }
36 |
37 | def lazy_init():
38 | """Initialize heavy components only when needed."""
39 | global _global_state
40 |
41 | if _global_state["initialized"]:
42 | return
43 |
44 | try:
45 | print("Lazy loading MCTS components...", file=sys.stderr)
46 |
47 | # Load environment variables
48 | from dotenv import load_dotenv
49 | load_dotenv()
50 |
51 | # Load config
52 | from .mcts_config import DEFAULT_CONFIG
53 | _global_state["config"] = DEFAULT_CONFIG.copy()
54 |
55 | # Set default provider from environment
56 | _global_state["active_llm_provider"] = os.getenv("DEFAULT_LLM_PROVIDER", "ollama")
57 | _global_state["active_model_name"] = os.getenv("DEFAULT_MODEL_NAME")
58 |
59 | # Initialize state manager
60 | from .state_manager import StateManager
61 | db_path = os.path.expanduser("~/.mcts_mcp_server/state.db")
62 | os.makedirs(os.path.dirname(db_path), exist_ok=True)
63 | _global_state["state_manager"] = StateManager(db_path)
64 |
65 | _global_state["initialized"] = True
66 | print("MCTS components loaded", file=sys.stderr)
67 |
68 | except Exception as e:
69 | print(f"Lazy init error: {e}", file=sys.stderr)
70 | logger.error(f"Lazy initialization failed: {e}")
71 |
72 | def get_or_create_background_loop():
73 | """Get or create a background event loop (lazy)."""
74 | global _global_state
75 |
76 | if _global_state["background_loop"] is None or _global_state["background_thread"] is None:
77 | loop_created = threading.Event()
78 | loop_container = {"loop": None}
79 |
80 | def create_background_loop():
81 | """Create and run a background event loop."""
82 | loop = asyncio.new_event_loop()
83 | asyncio.set_event_loop(loop)
84 | loop_container["loop"] = loop
85 | _global_state["background_loop"] = loop
86 | loop_created.set()
87 |
88 | try:
89 | loop.run_forever()
90 | except Exception as e:
91 | logger.error(f"Background loop error: {e}")
92 | finally:
93 | loop.close()
94 |
95 | # Start the background thread
96 | thread = threading.Thread(target=create_background_loop, daemon=True)
97 | thread.start()
98 | _global_state["background_thread"] = thread
99 |
100 | # Wait for loop to be created
101 | if not loop_created.wait(timeout=3.0):
102 | raise RuntimeError("Failed to create background event loop")
103 |
104 | return _global_state["background_loop"]
105 |
106 | def run_in_background_loop(coro):
107 | """Run a coroutine in the background event loop."""
108 | loop = get_or_create_background_loop()
109 |
110 | if loop.is_running():
111 | future = asyncio.run_coroutine_threadsafe(coro, loop)
112 | return future.result(timeout=300)
113 | else:
114 | raise RuntimeError("Background event loop is not running")
115 |
116 | def register_mcts_tools(mcp: FastMCP, db_path: str):
117 | """
118 | Register all MCTS-related tools with minimal startup delay.
119 | Heavy initialization is deferred until tools are actually used.
120 | """
121 | global _global_state
122 |
123 | print("Registering MCTS tools (fast mode)...", file=sys.stderr)
124 |
125 | # Store db_path for lazy initialization
126 | _global_state["db_path"] = db_path
127 |
128 | @mcp.tool()
129 | def initialize_mcts(question: str, chat_id: str, provider_name: Optional[str] = None,
130 | model_name: Optional[str] = None, config_updates: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
131 | """Initialize the MCTS system with lazy loading."""
132 | global _global_state
133 |
134 | try:
135 | # Trigger lazy initialization
136 | lazy_init()
137 |
138 | logger.info(f"Initializing MCTS for chat ID: {chat_id}")
139 |
140 | # Determine target provider and model
141 | target_provider = provider_name or _global_state["active_llm_provider"] or "ollama"
142 | target_model = model_name or _global_state["active_model_name"]
143 |
144 | logger.info(f"Using LLM Provider: {target_provider}, Model: {target_model}")
145 |
146 | # Update config if provided
147 | if config_updates:
148 | cfg = _global_state["config"].copy()
149 | cfg.update(config_updates)
150 | _global_state["config"] = cfg
151 | else:
152 | cfg = _global_state["config"]
153 |
154 | _global_state["current_chat_id"] = chat_id
155 | state_manager = _global_state["state_manager"]
156 | loaded_state = state_manager.load_state(chat_id) if cfg.get("enable_state_persistence", True) else None
157 |
158 | # Instantiate the appropriate adapter
159 | llm_adapter = None
160 | if target_provider == "ollama":
161 | from .ollama_adapter import OllamaAdapter
162 | if not target_model:
163 | target_model = OllamaAdapter.DEFAULT_MODEL
164 | llm_adapter = OllamaAdapter(model_name=target_model)
165 |
166 | elif target_provider == "openai":
167 | from .openai_adapter import OpenAIAdapter
168 | if not target_model:
169 | target_model = OpenAIAdapter.DEFAULT_MODEL
170 | llm_adapter = OpenAIAdapter(api_key=os.getenv("OPENAI_API_KEY"), model_name=target_model)
171 |
172 | elif target_provider == "anthropic":
173 | from .anthropic_adapter import AnthropicAdapter
174 | if not target_model:
175 | target_model = AnthropicAdapter.DEFAULT_MODEL
176 | llm_adapter = AnthropicAdapter(api_key=os.getenv("ANTHROPIC_API_KEY"), model_name=target_model)
177 |
178 | elif target_provider == "gemini":
179 | from .gemini_adapter import GeminiAdapter
180 | if not target_model:
181 | target_model = GeminiAdapter.DEFAULT_MODEL
182 | llm_adapter = GeminiAdapter(api_key=os.getenv("GEMINI_API_KEY"), model_name=target_model)
183 |
184 | else:
185 | return {"error": f"Unsupported LLM provider: {target_provider}", "status": "error"}
186 |
187 | _global_state["active_llm_provider"] = target_provider
188 | _global_state["active_model_name"] = target_model
189 |
190 | # Generate initial analysis using the background loop
191 | async def generate_initial():
192 | initial_prompt = f"<instruction>Provide an initial analysis of the following question. Be clear and concise.</instruction><question>{question}</question>"
193 | initial_messages = [{"role": "user", "content": initial_prompt}]
194 | return await llm_adapter.get_completion(model=target_model, messages=initial_messages)
195 |
196 | try:
197 | initial_analysis = run_in_background_loop(generate_initial())
198 | except Exception as e:
199 | logger.error(f"Failed to generate initial analysis: {e}")
200 | return {"error": f"Failed to generate initial analysis: {str(e)}", "status": "error"}
201 |
202 | # Create MCTS instance
203 | from .mcts_core import MCTS
204 | _global_state["mcts_instance"] = MCTS(
205 | llm_interface=llm_adapter,
206 | question=question,
207 | initial_analysis_content=initial_analysis or "No initial analysis available",
208 | config=cfg,
209 | initial_state=loaded_state
210 | )
211 |
212 | return {
213 | "status": "initialized",
214 | "question": question,
215 | "chat_id": chat_id,
216 | "initial_analysis": initial_analysis,
217 | "loaded_state": loaded_state is not None,
218 | "provider": target_provider,
219 | "model_used": target_model,
220 | "config": {k: v for k, v in cfg.items() if not k.startswith("_")},
221 | "run_id": _global_state.get("current_run_id")
222 | }
223 |
224 | except Exception as e:
225 | logger.error(f"Error in initialize_mcts: {e}")
226 | return {"error": f"Failed to initialize MCTS: {str(e)}", "status": "error"}
227 |
228 | @mcp.tool()
229 | def set_active_llm(provider_name: str, model_name: Optional[str] = None) -> Dict[str, Any]:
230 | """Set the active LLM provider and model."""
231 | global _global_state
232 |
233 | supported_providers = ["ollama", "openai", "anthropic", "gemini"]
234 | provider_name_lower = provider_name.lower()
235 |
236 | if provider_name_lower not in supported_providers:
237 | return {
238 | "status": "error",
239 | "message": f"Unsupported provider: '{provider_name}'. Supported: {supported_providers}"
240 | }
241 |
242 | _global_state["active_llm_provider"] = provider_name_lower
243 | _global_state["active_model_name"] = model_name
244 |
245 | log_msg = f"Set active LLM provider to: {provider_name_lower}."
246 | if model_name:
247 | log_msg += f" Set active model to: {model_name}."
248 |
249 | return {"status": "success", "message": log_msg}
250 |
251 | @mcp.tool()
252 | def list_ollama_models() -> Dict[str, Any]:
253 | """List all available Ollama models (with lazy loading)."""
254 | try:
255 | # Check if Ollama server is running (quick check)
256 | import httpx
257 | with httpx.Client(base_url="http://localhost:11434", timeout=2.0) as client:
258 | response = client.get("/")
259 | if response.status_code != 200:
260 | return {
261 | "status": "error",
262 | "message": "Ollama server not responding. Please ensure Ollama is running."
263 | }
264 | except Exception as e:
265 | return {
266 | "status": "error",
267 | "message": f"Cannot connect to Ollama server: {str(e)}"
268 | }
269 |
270 | # Now do the heavy model checking
271 | try:
272 | from .ollama_utils import check_available_models, get_recommended_models
273 | available_models = check_available_models()
274 |
275 | if not available_models:
276 | return {
277 | "status": "error",
278 | "message": "No Ollama models found. Try 'ollama pull MODEL_NAME' to download a model."
279 | }
280 |
281 | recommendations = get_recommended_models(available_models)
282 | current_model = _global_state.get("active_model_name") if _global_state.get("active_llm_provider") == "ollama" else None
283 |
284 | # Update global state
285 | _global_state["ollama_available_models"] = available_models
286 |
287 | return {
288 | "status": "success",
289 | "ollama_available_models": available_models,
290 | "current_ollama_model": current_model,
291 | "recommended_small_models": recommendations["small_models"],
292 | "recommended_medium_models": recommendations["medium_models"],
293 | "message": f"Found {len(available_models)} Ollama models"
294 | }
295 |
296 | except Exception as e:
297 | return {
298 | "status": "error",
299 | "message": f"Error listing Ollama models: {str(e)}"
300 | }
301 |
302 | @mcp.tool()
303 | def run_mcts(iterations: int = 1, simulations_per_iteration: int = 5, model_name: Optional[str] = None) -> Dict[str, Any]:
304 | """Run the MCTS algorithm."""
305 | global _global_state
306 |
307 | mcts = _global_state.get("mcts_instance")
308 | if not mcts:
309 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
310 |
311 | active_provider = _global_state.get("active_llm_provider")
312 | active_model = _global_state.get("active_model_name")
313 |
314 | if not active_provider or not active_model:
315 | return {"error": "Active LLM provider or model not set."}
316 |
317 | # Update config for this run
318 | temp_config = mcts.config.copy()
319 | temp_config["max_iterations"] = iterations
320 | temp_config["simulations_per_iteration"] = simulations_per_iteration
321 | mcts.config = temp_config
322 |
323 | logger.info(f"Starting MCTS run with {iterations} iterations, {simulations_per_iteration} simulations per iteration")
324 |
325 | def run_mcts_background():
326 | """Run MCTS in background thread."""
327 | try:
328 | async def run_search():
329 | await mcts.run_search_iterations(iterations, simulations_per_iteration)
330 | return mcts.get_final_results()
331 |
332 | results = run_in_background_loop(run_search())
333 |
334 | # Save state if enabled
335 | if temp_config.get("enable_state_persistence", True) and _global_state["current_chat_id"]:
336 | try:
337 | _global_state["state_manager"].save_state(_global_state["current_chat_id"], mcts)
338 | logger.info(f"Saved state for chat ID: {_global_state['current_chat_id']}")
339 | except Exception as e:
340 | logger.error(f"Error saving state: {e}")
341 |
342 | logger.info(f"MCTS run completed. Best score: {results.best_score if results else 0.0}")
343 |
344 | except Exception as e:
345 | logger.error(f"Error in background MCTS run: {e}")
346 |
347 | # Start background thread
348 | background_thread = threading.Thread(target=run_mcts_background)
349 | background_thread.daemon = True
350 | background_thread.start()
351 |
352 | return {
353 | "status": "started",
354 | "message": f"MCTS process started with {iterations} iterations and {simulations_per_iteration} simulations per iteration.",
355 | "provider": active_provider,
356 | "model": active_model,
357 | "background_thread_id": background_thread.ident
358 | }
359 |
360 | @mcp.tool()
361 | def generate_synthesis() -> Dict[str, Any]:
362 | """Generate a final synthesis of the MCTS results."""
363 | global _global_state
364 |
365 | mcts = _global_state.get("mcts_instance")
366 | if not mcts:
367 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
368 |
369 | try:
370 | async def synth():
371 | llm_adapter = mcts.llm
372 | path_nodes = mcts.get_best_path_nodes()
373 |
374 | from .utils import truncate_text
375 |
376 | path_thoughts_list = [
377 | f"- (Node {node.sequence}): {node.thought.strip()}"
378 | for node in path_nodes if node.thought and node.parent
379 | ]
380 | path_thoughts_str = "\n".join(path_thoughts_list) if path_thoughts_list else "No significant development path identified."
381 |
382 | results = mcts.get_final_results()
383 |
384 | synth_context = {
385 | "question_summary": mcts.question_summary,
386 | "initial_analysis_summary": truncate_text(mcts.root.content, 300) if mcts.root else "N/A",
387 | "best_score": f"{results.best_score:.1f}",
388 | "path_thoughts": path_thoughts_str,
389 | "final_best_analysis_summary": truncate_text(results.best_solution_content, 400),
390 | "previous_best_summary": "N/A",
391 | "unfit_markers_summary": "N/A",
392 | "learned_approach_summary": "N/A"
393 | }
394 |
395 | synthesis = await llm_adapter.synthesize_result(synth_context, mcts.config)
396 | best_node = mcts.find_best_final_node()
397 | tags = best_node.descriptive_tags if best_node else []
398 |
399 | return {
400 | "synthesis": synthesis,
401 | "best_score": results.best_score,
402 | "tags": tags,
403 | "iterations_completed": mcts.iterations_completed,
404 | "provider": _global_state.get("active_llm_provider"),
405 | "model": _global_state.get("active_model_name"),
406 | }
407 |
408 | synthesis_result = run_in_background_loop(synth())
409 | return synthesis_result
410 |
411 | except Exception as e:
412 | logger.error(f"Error generating synthesis: {e}")
413 | return {"error": f"Synthesis generation failed: {str(e)}"}
414 |
415 | @mcp.tool()
416 | def get_config() -> Dict[str, Any]:
417 | """Get the current MCTS configuration."""
418 | global _global_state
419 |
420 | # Trigger lazy init if needed
421 | if not _global_state["initialized"]:
422 | lazy_init()
423 |
424 | config = {k: v for k, v in _global_state["config"].items() if not k.startswith("_")}
425 | config.update({
426 | "active_llm_provider": _global_state.get("active_llm_provider"),
427 | "active_model_name": _global_state.get("active_model_name"),
428 | "ollama_available_models": _global_state.get("ollama_available_models", []),
429 | "current_run_id": _global_state.get("current_run_id")
430 | })
431 | return config
432 |
433 | @mcp.tool()
434 | def update_config(config_updates: Dict[str, Any]) -> Dict[str, Any]:
435 | """Update the MCTS configuration."""
436 | global _global_state
437 |
438 | # Trigger lazy init if needed
439 | if not _global_state["initialized"]:
440 | lazy_init()
441 |
442 | logger.info(f"Updating MCTS config with: {config_updates}")
443 |
444 | # Provider and model changes should use set_active_llm
445 | if "active_llm_provider" in config_updates or "active_model_name" in config_updates:
446 | logger.warning("Use 'set_active_llm' tool to change LLM provider or model.")
447 | config_updates.pop("active_llm_provider", None)
448 | config_updates.pop("active_model_name", None)
449 |
450 | # Update config
451 | cfg = _global_state["config"].copy()
452 | cfg.update(config_updates)
453 | _global_state["config"] = cfg
454 |
455 | mcts = _global_state.get("mcts_instance")
456 | if mcts:
457 | mcts.config = cfg
458 |
459 | return get_config()
460 |
461 | @mcp.tool()
462 | def get_mcts_status() -> Dict[str, Any]:
463 | """Get the current status of the MCTS system."""
464 | global _global_state
465 |
466 | mcts = _global_state.get("mcts_instance")
467 | if not mcts:
468 | return {
469 | "initialized": False,
470 | "message": "MCTS not initialized. Call initialize_mcts first."
471 | }
472 |
473 | try:
474 | from .utils import truncate_text
475 |
476 | best_node = mcts.find_best_final_node()
477 | tags = best_node.descriptive_tags if best_node else []
478 |
479 | return {
480 | "initialized": True,
481 | "chat_id": _global_state.get("current_chat_id"),
482 | "iterations_completed": getattr(mcts, "iterations_completed", 0),
483 | "simulations_completed": getattr(mcts, "simulations_completed", 0),
484 | "best_score": getattr(mcts, "best_score", 0.0),
485 | "best_content_summary": truncate_text(getattr(mcts, "best_solution", ""), 100),
486 | "tags": tags,
487 | "tree_depth": mcts.memory.get("depth", 0) if hasattr(mcts, "memory") else 0,
488 | "approach_types": getattr(mcts, "approach_types", []),
489 | "active_llm_provider": _global_state.get("active_llm_provider"),
490 | "active_model_name": _global_state.get("active_model_name"),
491 | "run_id": _global_state.get("current_run_id")
492 | }
493 | except Exception as e:
494 | logger.error(f"Error getting MCTS status: {e}")
495 | return {
496 | "initialized": True,
497 | "error": f"Error getting MCTS status: {str(e)}",
498 | "chat_id": _global_state.get("current_chat_id")
499 | }
500 |
501 | print("MCTS tools registered successfully", file=sys.stderr)
502 |
```
--------------------------------------------------------------------------------
/src/mcts_mcp_server/tools.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Fixed Tools for MCTS with proper async handling
5 | ===============================================
6 |
7 | This module fixes the async event loop issues in the MCTS MCP tools.
8 | """
9 | import asyncio
10 | import logging
11 | import os
12 | import threading
13 | from collections.abc import Coroutine
14 | from typing import Any
15 |
16 | from dotenv import load_dotenv
17 | from mcp.server.fastmcp import FastMCP
18 |
19 | from .mcts_config import DEFAULT_CONFIG
20 | from .mcts_core import MCTS
21 | from .ollama_adapter import OllamaAdapter
22 | from .ollama_utils import (
23 | OLLAMA_PYTHON_PACKAGE_AVAILABLE,
24 | check_available_models,
25 | get_recommended_models,
26 | )
27 | from .state_manager import StateManager
28 | from .utils import truncate_text
29 |
30 | logger = logging.getLogger(__name__)
31 |
32 | # Global state to maintain between tool calls
33 | _global_state = {
34 | "mcts_instance": None,
35 | "config": None,
36 | "state_manager": None,
37 | "current_chat_id": None,
38 | "active_llm_provider": os.getenv("DEFAULT_LLM_PROVIDER", "ollama"),
39 | "active_model_name": os.getenv("DEFAULT_MODEL_NAME"),
40 | "collect_results": False,
41 | "current_run_id": None,
42 | "ollama_available_models": [],
43 | "background_loop": None,
44 | "background_thread": None
45 | }
46 |
47 | def get_or_create_background_loop() -> asyncio.AbstractEventLoop | None:
48 | """
49 | Get or create a background event loop that runs in a dedicated thread.
50 |
51 | Returns:
52 | The background event loop, or None if creation failed
53 |
54 | Note:
55 | This ensures all async operations use the same event loop and avoids
56 | "bound to different event loop" issues common in MCP tools
57 | """
58 | global _global_state
59 |
60 | if _global_state["background_loop"] is None or _global_state["background_thread"] is None:
61 | loop_created = threading.Event() # Use threading.Event instead of asyncio.Event
62 | loop_container: dict[str, asyncio.AbstractEventLoop | None] = {"loop": None}
63 |
64 | def create_background_loop():
65 | """Create and run a background event loop."""
66 | loop = asyncio.new_event_loop()
67 | asyncio.set_event_loop(loop)
68 | loop_container["loop"] = loop
69 | _global_state["background_loop"] = loop
70 | loop_created.set()
71 |
72 | try:
73 | loop.run_forever()
74 | except Exception as e:
75 | logger.error(f"Background loop error: {e}")
76 | finally:
77 | loop.close()
78 |
79 | # Start the background thread
80 | thread = threading.Thread(target=create_background_loop, daemon=True)
81 | thread.start()
82 | _global_state["background_thread"] = thread
83 |
84 | # Wait for loop to be created (with shorter timeout to avoid hanging)
85 | if not loop_created.wait(timeout=2.0):
86 | logger.warning("Background loop creation timed out")
87 | # Don't raise an error, just return None and handle gracefully
88 | return None
89 |
90 | if loop_container["loop"] is None:
91 | logger.warning("Failed to create background event loop")
92 | return None
93 |
94 | return _global_state["background_loop"]
95 |
96 | def run_in_background_loop(coro: Coroutine[Any, Any, Any]) -> Any:
97 | """
98 | Run a coroutine in the background event loop.
99 |
100 | Args:
101 | coro: The coroutine to execute
102 |
103 | Returns:
104 | The result of the coroutine execution
105 |
106 | Raises:
107 | RuntimeError: If all execution methods fail
108 |
109 | Note:
110 | This avoids the "bound to different event loop" issue by using
111 | a dedicated background loop with fallback strategies
112 | """
113 | loop = get_or_create_background_loop()
114 |
115 | if loop is None:
116 | # Fallback: try to run in a new event loop if background loop failed
117 | logger.warning("Background loop not available, using fallback")
118 | try:
119 | return asyncio.run(coro)
120 | except RuntimeError:
121 | # If we're already in an event loop, use thread executor
122 | try:
123 | import concurrent.futures
124 | with concurrent.futures.ThreadPoolExecutor() as executor:
125 | future = executor.submit(asyncio.run, coro)
126 | return future.result(timeout=300)
127 | except Exception as e:
128 | raise RuntimeError(f"Failed to run coroutine: {e}") from e
129 |
130 | if loop.is_running():
131 | # Submit to the running loop and wait for result
132 | future = asyncio.run_coroutine_threadsafe(coro, loop)
133 | return future.result(timeout=300) # 5 minute timeout
134 | else:
135 | # This shouldn't happen if the background loop is properly managed
136 | raise RuntimeError("Background event loop is not running")
137 |
138 | def register_mcts_tools(mcp: FastMCP, db_path: str) -> None:
139 | """
140 | Register all MCTS-related tools with the MCP server.
141 |
142 | Args:
143 | mcp: The FastMCP server instance to register tools with
144 | db_path: Path to the SQLite database for state persistence
145 |
146 | Note:
147 | Initializes global state, loads environment variables, and registers
148 | all tool functions with proper async handling
149 | """
150 | global _global_state
151 |
152 | # Load environment variables
153 | load_dotenv()
154 |
155 | # Initialize state manager
156 | _global_state["state_manager"] = StateManager(db_path)
157 |
158 | # Initialize config
159 | _global_state["config"] = DEFAULT_CONFIG.copy()
160 |
161 | # Don't check Ollama models during initialization to prevent hanging
162 | # Models will be checked when list_ollama_models() is called
163 | _global_state["ollama_available_models"] = []
164 |
165 | # Set default model for ollama if needed
166 | if _global_state["active_llm_provider"] == "ollama" and not _global_state["active_model_name"]:
167 | _global_state["active_model_name"] = OllamaAdapter.DEFAULT_MODEL
168 |
169 | @mcp.tool()
170 | def initialize_mcts(question: str, chat_id: str, provider_name: str | None = None,
171 | model_name: str | None = None, config_updates: dict[str, Any] | None = None) -> dict[str, Any]:
172 | """
173 | Initialize the MCTS system with proper async handling.
174 |
175 | Args:
176 | question: The question or topic to analyze
177 | chat_id: Unique identifier for this conversation session
178 | provider_name: LLM provider to use (ollama, openai, anthropic, gemini)
179 | model_name: Specific model name to use
180 | config_updates: Optional configuration overrides
181 |
182 | Returns:
183 | dictionary containing initialization status, configuration, and metadata
184 |
185 | Note:
186 | Creates LLM adapter, generates initial analysis, and sets up MCTS instance
187 | with optional state loading from previous sessions
188 | """
189 | global _global_state
190 |
191 | try:
192 | logger.info(f"Initializing MCTS for chat ID: {chat_id}")
193 |
194 | # Determine target provider and model
195 | target_provider = provider_name or _global_state["active_llm_provider"]
196 | target_model = model_name or _global_state["active_model_name"]
197 |
198 | logger.info(f"Using LLM Provider: {target_provider}, Model: {target_model}")
199 |
200 | # Update config if provided
201 | if config_updates:
202 | cfg = _global_state["config"].copy()
203 | cfg.update(config_updates)
204 | _global_state["config"] = cfg
205 | else:
206 | cfg = _global_state["config"]
207 |
208 | _global_state["current_chat_id"] = chat_id
209 | state_manager = _global_state["state_manager"]
210 | loaded_state = state_manager.load_state(chat_id) if cfg.get("enable_state_persistence", True) else None
211 |
212 | # Instantiate the appropriate adapter
213 | llm_adapter = None
214 | if target_provider == "ollama":
215 | if not target_model:
216 | target_model = OllamaAdapter.DEFAULT_MODEL
217 | if target_model not in _global_state["ollama_available_models"]:
218 | return {
219 | "status": "model_error",
220 | "error": f"Ollama model '{target_model}' not available",
221 | "available_models": _global_state["ollama_available_models"]
222 | }
223 | llm_adapter = OllamaAdapter(model_name=target_model)
224 |
225 | elif target_provider == "openai":
226 | from .openai_adapter import OpenAIAdapter
227 | if not target_model:
228 | target_model = OpenAIAdapter.DEFAULT_MODEL
229 | llm_adapter = OpenAIAdapter(api_key=os.getenv("OPENAI_API_KEY"), model_name=target_model)
230 |
231 | elif target_provider == "anthropic":
232 | from .anthropic_adapter import AnthropicAdapter
233 | if not target_model:
234 | target_model = AnthropicAdapter.DEFAULT_MODEL
235 | llm_adapter = AnthropicAdapter(api_key=os.getenv("ANTHROPIC_API_KEY"), model_name=target_model)
236 |
237 | elif target_provider == "gemini":
238 | from .gemini_adapter import GeminiAdapter
239 | if not target_model:
240 | target_model = GeminiAdapter.DEFAULT_MODEL
241 | llm_adapter = GeminiAdapter(api_key=os.getenv("GEMINI_API_KEY"), model_name=target_model)
242 |
243 | else:
244 | return {"error": f"Unsupported LLM provider: {target_provider}", "status": "error"}
245 |
246 | _global_state["active_llm_provider"] = target_provider
247 | _global_state["active_model_name"] = target_model
248 |
249 | # Generate initial analysis using the background loop
250 | async def generate_initial():
251 | initial_prompt = f"<instruction>Provide an initial analysis of the following question. Be clear and concise.</instruction><question>{question}</question>"
252 | initial_messages = [{"role": "user", "content": initial_prompt}]
253 | return await llm_adapter.get_completion(model=target_model, messages=initial_messages)
254 |
255 | try:
256 | initial_analysis = run_in_background_loop(generate_initial())
257 | except Exception as e:
258 | logger.error(f"Failed to generate initial analysis: {e}")
259 | return {"error": f"Failed to generate initial analysis: {str(object=e)}", "status": "error"}
260 |
261 | # Create MCTS instance
262 | _global_state["mcts_instance"] = MCTS(
263 | llm_interface=llm_adapter,
264 | question=question,
265 | initial_analysis_content=initial_analysis or "No initial analysis available",
266 | config=cfg,
267 | initial_state=loaded_state
268 | )
269 |
270 | return {
271 | "status": "initialized",
272 | "question": question,
273 | "chat_id": chat_id,
274 | "initial_analysis": initial_analysis,
275 | "loaded_state": loaded_state is not None,
276 | "provider": target_provider,
277 | "model_used": target_model,
278 | "config": {k: v for k, v in cfg.items() if not k.startswith("_")},
279 | "run_id": _global_state.get("current_run_id")
280 | }
281 |
282 | except ValueError as ve:
283 | logger.error(f"Configuration error: {ve}")
284 | return {"error": f"Configuration error: {ve!s}", "status": "config_error"}
285 | except Exception as e:
286 | logger.error(f"Error in initialize_mcts: {e}")
287 | return {"error": f"Failed to initialize MCTS: {e!s}", "status": "error"}
288 |
289 | @mcp.tool()
290 | def set_active_llm(provider_name: str, model_name: str | None = None) -> dict[str, Any]:
291 | """
292 | Set the active LLM provider and model for subsequent operations.
293 |
294 | Args:
295 | provider_name: Name of the LLM provider (ollama, openai, anthropic, gemini)
296 | model_name: Optional specific model name to use
297 |
298 | Returns:
299 | dictionary containing status and confirmation message
300 |
301 | Note:
302 | Changes the global LLM configuration but doesn't affect already
303 | initialized MCTS instances
304 | """
305 | global _global_state
306 | supported_providers = ["ollama", "openai", "anthropic", "gemini"]
307 | provider_name_lower = provider_name.lower()
308 |
309 | if provider_name_lower not in supported_providers:
310 | return {
311 | "status": "error",
312 | "message": f"Unsupported provider: '{provider_name}'. Supported: {supported_providers}"
313 | }
314 |
315 | _global_state["active_llm_provider"] = provider_name_lower
316 | _global_state["active_model_name"] = model_name
317 |
318 | log_msg = f"Set active LLM provider to: {provider_name_lower}."
319 | if model_name:
320 | log_msg += f" Set active model to: {model_name}."
321 |
322 | return {"status": "success", "message": log_msg}
323 |
324 | @mcp.tool()
325 | def list_ollama_models() -> dict[str, Any]:
326 | """
327 | List all available Ollama models with recommendations.
328 |
329 | Returns:
330 | dictionary containing:
331 | - status: Success or error status
332 | - ollama_available_models: List of all available models
333 | - current_ollama_model: Currently active model
334 | - recommended_small_models: Models suitable for basic tasks
335 | - recommended_medium_models: Models for complex analysis
336 | - message: Status message
337 |
338 | Note:
339 | Checks Ollama server connectivity and updates global model cache
340 | """
341 | logger.info("Listing Ollama models...")
342 |
343 | # Check if Ollama server is running
344 | try:
345 | import httpx
346 | with httpx.Client(base_url="http://localhost:11434", timeout=3.0) as client:
347 | response = client.get("/")
348 | if response.status_code != 200:
349 | return {
350 | "status": "error",
351 | "message": "Ollama server not responding. Please ensure Ollama is running."
352 | }
353 | except Exception as e:
354 | return {
355 | "status": "error",
356 | "message": f"Cannot connect to Ollama server: {e!s}"
357 | }
358 |
359 | # Get available models
360 | available_models = check_available_models()
361 | if not available_models:
362 | return {
363 | "status": "error",
364 | "message": "No Ollama models found. Try 'ollama pull MODEL_NAME' to download a model."
365 | }
366 |
367 | # Get recommendations
368 | recommendations = get_recommended_models(available_models)
369 | current_model = _global_state.get("active_model_name") if _global_state.get("active_llm_provider") == "ollama" else None
370 |
371 | # Update global state
372 | _global_state["ollama_available_models"] = available_models
373 |
374 | return {
375 | "status": "success",
376 | "ollama_available_models": available_models,
377 | "current_ollama_model": current_model,
378 | "recommended_small_models": recommendations["small_models"],
379 | "recommended_medium_models": recommendations["medium_models"],
380 | "message": f"Found {len(available_models)} Ollama models"
381 | }
382 |
383 | @mcp.tool()
384 | def run_mcts(iterations: int = 1, simulations_per_iteration: int = 5, model_name: str | None = None) -> dict[str, Any]:
385 | """
386 | Run the MCTS algorithm with proper async handling.
387 |
388 | Args:
389 | iterations: Number of MCTS iterations to run
390 | simulations_per_iteration: Number of simulations per iteration
391 | model_name: Optional model override (currently unused)
392 |
393 | Returns:
394 | dictionary containing:
395 | - status: 'started' if successful
396 | - message: Confirmation message
397 | - provider: Active LLM provider
398 | - model: Active model name
399 | - background_thread_id: Thread ID for monitoring
400 |
401 | Note:
402 | Runs MCTS in a background thread to avoid blocking the MCP server
403 | Automatically saves state if persistence is enabled
404 | """
405 | global _global_state
406 |
407 | mcts = _global_state.get("mcts_instance")
408 | if not mcts:
409 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
410 |
411 | active_provider = _global_state.get("active_llm_provider")
412 | active_model = _global_state.get("active_model_name")
413 |
414 | if not active_provider or not active_model:
415 | return {"error": "Active LLM provider or model not set."}
416 |
417 | # Update config for this run
418 | temp_config = mcts.config.copy()
419 | temp_config["max_iterations"] = iterations
420 | temp_config["simulations_per_iteration"] = simulations_per_iteration
421 | mcts.config = temp_config
422 |
423 | logger.info(f"Starting MCTS run with {iterations} iterations, {simulations_per_iteration} simulations per iteration")
424 |
425 | def run_mcts_background():
426 | """Run MCTS in background thread with proper async handling."""
427 | try:
428 | # Use the background loop for all async operations
429 | async def run_search():
430 | await mcts.run_search_iterations(iterations, simulations_per_iteration)
431 | return mcts.get_final_results()
432 |
433 | results = run_in_background_loop(run_search())
434 |
435 | # Save state if enabled
436 | if temp_config.get("enable_state_persistence", True) and _global_state["current_chat_id"]:
437 | try:
438 | _global_state["state_manager"].save_state(_global_state["current_chat_id"], mcts)
439 | logger.info(f"Saved state for chat ID: {_global_state['current_chat_id']}")
440 | except Exception as e:
441 | logger.error(f"Error saving state: {e}")
442 |
443 | # Get best node and tags
444 | best_node = mcts.find_best_final_node()
445 | tags = best_node.descriptive_tags if best_node else []
446 |
447 | # Log the tags for debugging/monitoring
448 | if tags:
449 | logger.info(f"Best node tags: {', '.join(tags)}")
450 |
451 | logger.info(f"MCTS run completed. Best score: {results.best_score if results else 0.0}")
452 |
453 | except Exception as e:
454 | logger.error(f"Error in background MCTS run: {e}")
455 |
456 | # Start background thread
457 | background_thread = threading.Thread(target=run_mcts_background)
458 | background_thread.daemon = True
459 | background_thread.start()
460 |
461 | return {
462 | "status": "started",
463 | "message": f"MCTS process started with {iterations} iterations and {simulations_per_iteration} simulations per iteration.",
464 | "provider": active_provider,
465 | "model": active_model,
466 | "background_thread_id": background_thread.ident
467 | }
468 |
469 | @mcp.tool()
470 | def generate_synthesis() -> dict[str, Any]:
471 | """
472 | Generate a final synthesis of the MCTS results.
473 |
474 | Returns:
475 | dictionary containing:
476 | - synthesis: Generated synthesis text
477 | - best_score: Best score achieved during search
478 | - tags: Descriptive tags from best analysis
479 | - iterations_completed: Number of iterations completed
480 | - provider: LLM provider used
481 | - model: Model used
482 |
483 | Raises:
484 | Returns error dict if MCTS not initialized or synthesis fails
485 |
486 | Note:
487 | Uses the same background loop as MCTS to ensure consistency
488 | """
489 | global _global_state
490 |
491 | mcts = _global_state.get("mcts_instance")
492 | if not mcts:
493 | return {"error": "MCTS not initialized. Call initialize_mcts first."}
494 |
495 | try:
496 | async def synth():
497 | llm_adapter = mcts.llm
498 | path_nodes = mcts.get_best_path_nodes()
499 |
500 | path_thoughts_list = [
501 | f"- (Node {node.sequence}): {node.thought.strip()}"
502 | for node in path_nodes if node.thought and node.parent
503 | ]
504 | path_thoughts_str = "\n".join(path_thoughts_list) if path_thoughts_list else "No significant development path identified."
505 |
506 | results = mcts.get_final_results()
507 |
508 | synth_context = {
509 | "question_summary": mcts.question_summary,
510 | "initial_analysis_summary": truncate_text(mcts.root.content, 300) if mcts.root else "N/A",
511 | "best_score": f"{results.best_score:.1f}",
512 | "path_thoughts": path_thoughts_str,
513 | "final_best_analysis_summary": truncate_text(results.best_solution_content, 400),
514 | "previous_best_summary": "N/A",
515 | "unfit_markers_summary": "N/A",
516 | "learned_approach_summary": "N/A"
517 | }
518 |
519 | synthesis = await llm_adapter.synthesize_result(synth_context, mcts.config)
520 | best_node = mcts.find_best_final_node()
521 | tags = best_node.descriptive_tags if best_node else []
522 |
523 | return {
524 | "synthesis": synthesis,
525 | "best_score": results.best_score,
526 | "tags": tags,
527 | "iterations_completed": mcts.iterations_completed,
528 | "provider": _global_state.get("active_llm_provider"),
529 | "model": _global_state.get("active_model_name"),
530 | }
531 |
532 | # Use the background loop for synthesis generation
533 | synthesis_result = run_in_background_loop(synth())
534 | return synthesis_result
535 |
536 | except Exception as e:
537 | logger.error(f"Error generating synthesis: {e}")
538 | return {"error": f"Synthesis generation failed: {e!s}"}
539 |
540 | @mcp.tool()
541 | def get_config() -> dict[str, Any]:
542 | """
543 | Get the current MCTS configuration and system status.
544 |
545 | Returns:
546 | dictionary containing all configuration parameters, active LLM settings,
547 | and system capabilities
548 |
549 | Note:
550 | Filters out internal configuration keys starting with underscore
551 | """
552 | global _global_state
553 | config = {k: v for k, v in _global_state["config"].items() if not k.startswith("_")}
554 | config.update({
555 | "active_llm_provider": _global_state.get("active_llm_provider"),
556 | "active_model_name": _global_state.get("active_model_name"),
557 | "ollama_python_package_available": OLLAMA_PYTHON_PACKAGE_AVAILABLE,
558 | "ollama_available_models": _global_state.get("ollama_available_models", []),
559 | "current_run_id": _global_state.get("current_run_id")
560 | })
561 | return config
562 |
563 | @mcp.tool()
564 | def update_config(config_updates: dict[str, Any]) -> dict[str, Any]:
565 | """
566 | Update the MCTS configuration parameters.
567 |
568 | Args:
569 | config_updates: dictionary of configuration keys and new values
570 |
571 | Returns:
572 | Updated configuration dictionary
573 |
574 | Note:
575 | Provider and model changes are ignored - use set_active_llm instead
576 | Updates both global config and active MCTS instance if present
577 | """
578 | global _global_state
579 |
580 | logger.info(f"Updating MCTS config with: {config_updates}")
581 |
582 | # Provider and model changes should use set_active_llm
583 | if "active_llm_provider" in config_updates or "active_model_name" in config_updates:
584 | logger.warning("Use 'set_active_llm' tool to change LLM provider or model.")
585 | config_updates.pop("active_llm_provider", None)
586 | config_updates.pop("active_model_name", None)
587 |
588 | # Update config
589 | cfg = _global_state["config"].copy()
590 | cfg.update(config_updates)
591 | _global_state["config"] = cfg
592 |
593 | mcts = _global_state.get("mcts_instance")
594 | if mcts:
595 | mcts.config = cfg
596 |
597 | return get_config()
598 |
599 | @mcp.tool()
600 | def get_mcts_status() -> dict[str, Any]:
601 | """
602 | Get the current status of the MCTS system.
603 |
604 | Returns:
605 | dictionary containing:
606 | - initialized: Whether MCTS is initialized
607 | - chat_id: Current chat session ID
608 | - iterations_completed: Number of iterations run
609 | - simulations_completed: Total simulations run
610 | - best_score: Best score achieved
611 | - best_content_summary: Truncated best solution
612 | - tags: Tags from best analysis
613 | - tree_depth: Maximum tree depth explored
614 | - approach_types: List of analytical approaches used
615 | - active_llm_provider: Current LLM provider
616 | - active_model_name: Current model name
617 | - run_id: Current run identifier
618 |
619 | Note:
620 | Provides comprehensive status for monitoring and debugging
621 | """
622 | global _global_state
623 |
624 | mcts = _global_state.get("mcts_instance")
625 | if not mcts:
626 | return {
627 | "initialized": False,
628 | "message": "MCTS not initialized. Call initialize_mcts first."
629 | }
630 |
631 | try:
632 | best_node = mcts.find_best_final_node()
633 | tags = best_node.descriptive_tags if best_node else []
634 |
635 | return {
636 | "initialized": True,
637 | "chat_id": _global_state.get("current_chat_id"),
638 | "iterations_completed": getattr(mcts, "iterations_completed", 0),
639 | "simulations_completed": getattr(mcts, "simulations_completed", 0),
640 | "best_score": getattr(mcts, "best_score", 0.0),
641 | "best_content_summary": truncate_text(getattr(mcts, "best_solution", ""), 100),
642 | "tags": tags,
643 | "tree_depth": mcts.memory.get("depth", 0) if hasattr(mcts, "memory") else 0,
644 | "approach_types": getattr(mcts, "approach_types", []),
645 | "active_llm_provider": _global_state.get("active_llm_provider"),
646 | "active_model_name": _global_state.get("active_model_name"),
647 | "run_id": _global_state.get("current_run_id")
648 | }
649 | except Exception as e:
650 | logger.error(f"Error getting MCTS status: {e}")
651 | return {
652 | "initialized": True,
653 | "error": f"Error getting MCTS status: {e!s}",
654 | "chat_id": _global_state.get("current_chat_id")
655 | }
656 |
657 | @mcp.tool()
658 | def run_model_comparison(question: str, iterations: int = 2, simulations_per_iteration: int = 10) -> dict[str, Any]:
659 | """
660 | Run MCTS across multiple models for comparison analysis.
661 |
662 | Args:
663 | question: The question to analyze across models
664 | iterations: Number of MCTS iterations per model
665 | simulations_per_iteration: Simulations per iteration
666 |
667 | Returns:
668 | dictionary containing comparison setup or error information
669 |
670 | Note:
671 | Currently returns a placeholder - full implementation requires
672 | additional coordination between multiple MCTS instances
673 | """
674 | if not OLLAMA_PYTHON_PACKAGE_AVAILABLE:
675 | return {"error": "Ollama python package not available for model comparison."}
676 |
677 | # Get available models
678 | models = check_available_models()
679 | recommendations = get_recommended_models(models)
680 | comparison_models = recommendations["small_models"]
681 |
682 | if not comparison_models:
683 | return {"error": f"No suitable models found for comparison. Available: {models}"}
684 |
685 | return {
686 | "status": "started",
687 | "message": "Model comparison feature available but not implemented in this version",
688 | "question": question,
689 | "models": comparison_models,
690 | "iterations": iterations,
691 | "simulations_per_iteration": simulations_per_iteration
692 | }
693 |
```
--------------------------------------------------------------------------------
/src/mcts_mcp_server/analysis_tools/results_processor.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | MCTS Results Processor
5 | =====================
6 |
7 | This module provides a class for processing, analyzing, and extracting insights
8 | from MCTS run results. It helps identify the most valuable information in MCTS
9 | outputs and present it in a more structured and useful format.
10 | """
11 |
12 | import os
13 | import json
14 | import logging
15 | import datetime
16 | from typing import Dict, Any, List, Optional, Tuple, Set, Union
17 | import re
18 | from pathlib import Path
19 |
20 | logger = logging.getLogger("mcts_analysis")
21 |
22 | class ResultsProcessor:
23 | """Processes and analyzes MCTS run results to extract key insights."""
24 |
25 | def __init__(self, results_base_dir: Optional[str] = None):
26 | """
27 | Initialize the results processor.
28 |
29 | Args:
30 | results_base_dir: Base directory for MCTS results. If None, defaults to
31 | the standard location.
32 | """
33 | if results_base_dir is None:
34 | # Default to 'results' in the repository root
35 | repo_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
36 | self.results_base_dir = os.path.join(repo_dir, "results")
37 | else:
38 | self.results_base_dir = results_base_dir
39 |
40 | logger.info(f"Initialized ResultsProcessor with base directory: {self.results_base_dir}")
41 |
42 | # Cache for analyzed results
43 | self._cache = {}
44 |
45 | def list_runs(self, count: int = 10, model: Optional[str] = None) -> List[Dict[str, Any]]:
46 | """
47 | List recent MCTS runs with key metadata.
48 |
49 | Args:
50 | count: Maximum number of runs to return
51 | model: Optional model name to filter by
52 |
53 | Returns:
54 | List of run dictionaries with key metadata
55 | """
56 | runs = []
57 |
58 | # Walk through the results directory
59 | for model_dir in os.listdir(self.results_base_dir):
60 | # Skip if filtering by model and not matching
61 | if model and model != model_dir:
62 | continue
63 |
64 | model_path = os.path.join(self.results_base_dir, model_dir)
65 | if not os.path.isdir(model_path):
66 | continue
67 |
68 | # Check each run directory
69 | for run_dir in os.listdir(model_path):
70 | run_path = os.path.join(model_path, run_dir)
71 | if not os.path.isdir(run_path):
72 | continue
73 |
74 | # Try to load metadata
75 | metadata_path = os.path.join(run_path, "metadata.json")
76 | if not os.path.exists(metadata_path):
77 | continue
78 |
79 | try:
80 | with open(metadata_path, 'r') as f:
81 | metadata = json.load(f)
82 |
83 | # Extract key information
84 | run_info = {
85 | "run_id": metadata.get("run_id", run_dir),
86 | "model": metadata.get("model_name", model_dir),
87 | "question": metadata.get("question", "Unknown"),
88 | "timestamp": metadata.get("timestamp", 0),
89 | "timestamp_readable": metadata.get("timestamp_readable", "Unknown"),
90 | "status": metadata.get("status", "Unknown"),
91 | "score": metadata.get("results", {}).get("best_score", 0),
92 | "iterations": metadata.get("results", {}).get("iterations_completed", 0),
93 | "simulations": metadata.get("results", {}).get("simulations_completed", 0),
94 | "tags": metadata.get("results", {}).get("tags", []),
95 | "path": run_path
96 | }
97 |
98 | runs.append(run_info)
99 | except Exception as e:
100 | logger.warning(f"Failed to parse metadata from {metadata_path}: {e}")
101 |
102 | # Sort by timestamp (newest first)
103 | runs.sort(key=lambda r: r.get("timestamp", 0), reverse=True)
104 |
105 | # Limit to the requested count
106 | return runs[:count]
107 |
108 | def get_run_details(self, run_id: str) -> Optional[Dict[str, Any]]:
109 | """
110 | Get detailed information about a specific run.
111 |
112 | Args:
113 | run_id: Run ID or path to the run directory
114 |
115 | Returns:
116 | Dictionary with detailed run information or None if not found
117 | """
118 | # Handle the case where run_id is a path
119 | if os.path.isdir(run_id):
120 | run_path = run_id
121 | else:
122 | # Search for the run directory
123 | run_path = None
124 | for model_dir in os.listdir(self.results_base_dir):
125 | model_path = os.path.join(self.results_base_dir, model_dir)
126 | if not os.path.isdir(model_path):
127 | continue
128 |
129 | potential_path = os.path.join(model_path, run_id)
130 | if os.path.isdir(potential_path):
131 | run_path = potential_path
132 | break
133 |
134 | if run_path is None:
135 | logger.warning(f"Run not found: {run_id}")
136 | return None
137 |
138 | # Try to load metadata
139 | metadata_path = os.path.join(run_path, "metadata.json")
140 | if not os.path.exists(metadata_path):
141 | logger.warning(f"Metadata not found at {metadata_path}")
142 | return None
143 |
144 | try:
145 | with open(metadata_path, 'r') as f:
146 | metadata = json.load(f)
147 |
148 | # Load the best solution
149 | best_solution = ""
150 | solution_path = os.path.join(run_path, "best_solution.txt")
151 | if os.path.exists(solution_path):
152 | with open(solution_path, 'r') as f:
153 | best_solution = f.read()
154 |
155 | # Load progress information
156 | progress = []
157 | progress_path = os.path.join(run_path, "progress.jsonl")
158 | if os.path.exists(progress_path):
159 | with open(progress_path, 'r') as f:
160 | for line in f:
161 | if line.strip():
162 | try:
163 | progress.append(json.loads(line))
164 | except json.JSONDecodeError:
165 | pass
166 |
167 | # Combine everything into a single result
168 | result = {
169 | "metadata": metadata,
170 | "best_solution": best_solution,
171 | "progress": progress,
172 | "run_path": run_path,
173 | "run_id": os.path.basename(run_path)
174 | }
175 |
176 | return result
177 | except Exception as e:
178 | logger.warning(f"Failed to load run details from {run_path}: {e}")
179 | return None
180 |
181 | def extract_key_concepts(self, solution_text: str) -> List[str]:
182 | """
183 | Extract key concepts from a solution text.
184 |
185 | Args:
186 | solution_text: The solution text to analyze
187 |
188 | Returns:
189 | List of key concepts extracted from the text
190 | """
191 | # Look for sections explicitly labeled as key concepts
192 | key_concepts_match = re.search(r'Key Concepts:(.+?)($|(?:\n\n))', solution_text, re.DOTALL)
193 | if key_concepts_match:
194 | # Extract and clean concepts
195 | concepts_text = key_concepts_match.group(1)
196 | concepts = [c.strip().strip('-*•') for c in concepts_text.strip().split('\n')
197 | if c.strip() and not c.strip().startswith('#')]
198 | return [c for c in concepts if c]
199 |
200 | # Fallback: Look for bulleted or numbered lists
201 | bullet_matches = re.findall(r'(?:^|\n)[ \t]*[-•*][ \t]*(.*?)(?:$|\n)', solution_text)
202 | if bullet_matches:
203 | return [m.strip() for m in bullet_matches if m.strip()]
204 |
205 | # Last resort: Split paragraphs and take short ones as potential concepts
206 | paragraphs = [p.strip() for p in re.split(r'\n\s*\n', solution_text) if p.strip()]
207 | return [p for p in paragraphs if len(p) < 100 and len(p.split()) < 15][:5]
208 |
209 | def extract_key_arguments(self, solution_text: str) -> Dict[str, List[str]]:
210 | """
211 | Extract key arguments for and against from solution text.
212 |
213 | Args:
214 | solution_text: The solution text to analyze
215 |
216 | Returns:
217 | Dictionary with 'for' and 'against' keys mapping to lists of arguments
218 | """
219 | arguments = {"for": [], "against": []}
220 |
221 | # Look for "Arguments For" section
222 | for_match = re.search(r'(?:Key )?Arguments For.*?:(.+?)(?:\n\n|\n(?:Arguments|Against))',
223 | solution_text, re.DOTALL | re.IGNORECASE)
224 | if for_match:
225 | for_text = for_match.group(1).strip()
226 | # Extract bullet points
227 | for_args = [a.strip().strip('-*•') for a in re.findall(r'(?:^|\n)[ \t]*[-•*\d\.][ \t]*(.*?)(?:$|\n)', for_text)]
228 | arguments["for"] = [a for a in for_args if a]
229 |
230 | # Look for "Arguments Against" section
231 | against_match = re.search(r'(?:Key )?Arguments Against.*?:(.+?)(?:\n\n|\n(?:[A-Z]))',
232 | solution_text, re.DOTALL | re.IGNORECASE)
233 | if against_match:
234 | against_text = against_match.group(1).strip()
235 | # Extract bullet points
236 | against_args = [a.strip().strip('-*•') for a in re.findall(r'(?:^|\n)[ \t]*[-•*\d\.][ \t]*(.*?)(?:$|\n)', against_text)]
237 | arguments["against"] = [a for a in against_args if a]
238 |
239 | return arguments
240 |
241 | def extract_conclusions(self, solution_text: str, progress: List[Dict[str, Any]]) -> List[str]:
242 | """
243 | Extract conclusions from a solution and progress syntheses.
244 |
245 | Args:
246 | solution_text: The best solution text
247 | progress: Progress information including syntheses
248 |
249 | Returns:
250 | List of key conclusions
251 | """
252 | conclusions = []
253 |
254 | # Extract any section labeled "Conclusion" or at the end of the text
255 | conclusion_match = re.search(r'(?:^|\n)Conclusion:?\s*(.*?)(?:$|\n\n)', solution_text, re.DOTALL | re.IGNORECASE)
256 | if conclusion_match:
257 | conclusion_text = conclusion_match.group(1).strip()
258 | conclusions.append(conclusion_text)
259 | else:
260 | # Try to extract the last paragraph as a potential conclusion
261 | paragraphs = [p.strip() for p in re.split(r'\n\s*\n', solution_text) if p.strip()]
262 | if paragraphs and not paragraphs[-1].startswith('#') and len(paragraphs[-1]) > 50:
263 | conclusions.append(paragraphs[-1])
264 |
265 | # Extract syntheses from progress
266 | for entry in progress:
267 | if "synthesis" in entry:
268 | # Take the last paragraph of each synthesis as a conclusion
269 | synthesis_paragraphs = [p.strip() for p in re.split(r'\n\s*\n', entry["synthesis"]) if p.strip()]
270 | if synthesis_paragraphs:
271 | conclusions.append(synthesis_paragraphs[-1])
272 |
273 | # Remove duplicates and very similar conclusions
274 | unique_conclusions = []
275 | for c in conclusions:
276 | if not any(self._text_similarity(c, uc) > 0.7 for uc in unique_conclusions):
277 | unique_conclusions.append(c)
278 |
279 | return unique_conclusions
280 |
281 | def _text_similarity(self, text1: str, text2: str) -> float:
282 | """
283 | Calculate a simple similarity score between two texts.
284 |
285 | Args:
286 | text1: First text
287 | text2: Second text
288 |
289 | Returns:
290 | Similarity score between 0 and 1
291 | """
292 | # Normalize and tokenize
293 | words1 = set(re.findall(r'\w+', text1.lower()))
294 | words2 = set(re.findall(r'\w+', text2.lower()))
295 |
296 | # Calculate Jaccard similarity
297 | if not words1 or not words2:
298 | return 0.0
299 |
300 | intersection = len(words1.intersection(words2))
301 | union = len(words1.union(words2))
302 |
303 | return intersection / union
304 |
305 | def analyze_run(self, run_id: str) -> Dict[str, Any]:
306 | """
307 | Perform a comprehensive analysis of a run's results.
308 |
309 | Args:
310 | run_id: The run ID to analyze
311 |
312 | Returns:
313 | Dictionary with analysis results
314 | """
315 | # Check cache first
316 | if run_id in self._cache:
317 | return self._cache[run_id]
318 |
319 | # Get the run details
320 | run_details = self.get_run_details(run_id)
321 | if not run_details:
322 | return {"error": f"Run not found: {run_id}"}
323 |
324 | # Extract key information
325 | best_solution = run_details.get("best_solution", "")
326 | progress = run_details.get("progress", [])
327 | metadata = run_details.get("metadata", {})
328 |
329 | # Extract key insights
330 | key_concepts = self.extract_key_concepts(best_solution)
331 | key_arguments = self.extract_key_arguments(best_solution)
332 | conclusions = self.extract_conclusions(best_solution, progress)
333 |
334 | # Extract tags
335 | tags = metadata.get("results", {}).get("tags", [])
336 |
337 | # Prepare the analysis results
338 | analysis = {
339 | "run_id": run_id,
340 | "question": metadata.get("question", "Unknown"),
341 | "model": metadata.get("model_name", "Unknown"),
342 | "timestamp": metadata.get("timestamp_readable", "Unknown"),
343 | "duration": metadata.get("duration_seconds", 0),
344 | "status": metadata.get("status", "Unknown"),
345 | "best_score": metadata.get("results", {}).get("best_score", 0),
346 | "tags": tags,
347 | "key_concepts": key_concepts,
348 | "arguments_for": key_arguments["for"],
349 | "arguments_against": key_arguments["against"],
350 | "conclusions": conclusions,
351 | "path": run_details.get("run_path", "")
352 | }
353 |
354 | # Cache the results
355 | self._cache[run_id] = analysis
356 |
357 | return analysis
358 |
359 | def compare_runs(self, run_ids: List[str]) -> Dict[str, Any]:
360 | """
361 | Compare multiple runs to identify similarities and differences.
362 |
363 | Args:
364 | run_ids: List of run IDs to compare
365 |
366 | Returns:
367 | Dictionary with comparison results
368 | """
369 | # Analyze each run
370 | analyses = [self.analyze_run(run_id) for run_id in run_ids]
371 | analyses = [a for a in analyses if "error" not in a]
372 |
373 | if not analyses:
374 | return {"error": "No valid runs to compare"}
375 |
376 | # Extract shared and unique concepts
377 | all_concepts = [set(a.get("key_concepts", [])) for a in analyses]
378 | shared_concepts = set.intersection(*all_concepts) if all_concepts else set()
379 | unique_concepts = {}
380 |
381 | for i, a in enumerate(analyses):
382 | run_id = a.get("run_id", f"run_{i}")
383 | unique = all_concepts[i] - set.union(*[c for j, c in enumerate(all_concepts) if j != i])
384 | if unique:
385 | unique_concepts[run_id] = list(unique)
386 |
387 | # Get mean score
388 | scores = [a.get("best_score", 0) for a in analyses]
389 | mean_score = sum(scores) / len(scores) if scores else 0
390 |
391 | # Find common arguments
392 | all_args_for = [set(a.get("arguments_for", [])) for a in analyses]
393 | all_args_against = [set(a.get("arguments_against", [])) for a in analyses]
394 |
395 | shared_args_for = set.intersection(*all_args_for) if all_args_for else set()
396 | shared_args_against = set.intersection(*all_args_against) if all_args_against else set()
397 |
398 | # Prepare comparison results
399 | comparison = {
400 | "runs_compared": run_ids,
401 | "models": [a.get("model", "Unknown") for a in analyses],
402 | "mean_score": mean_score,
403 | "shared_concepts": list(shared_concepts),
404 | "unique_concepts": unique_concepts,
405 | "shared_arguments_for": list(shared_args_for),
406 | "shared_arguments_against": list(shared_args_against),
407 | "best_run": max(analyses, key=lambda a: a.get("best_score", 0)).get("run_id") if analyses else None
408 | }
409 |
410 | return comparison
411 |
412 | def get_best_runs(self, count: int = 5, min_score: float = 7.0) -> List[Dict[str, Any]]:
413 | """
414 | Get the best MCTS runs based on score.
415 |
416 | Args:
417 | count: Maximum number of runs to return
418 | min_score: Minimum score threshold
419 |
420 | Returns:
421 | List of best run analyses
422 | """
423 | # List all runs
424 | all_runs = self.list_runs(count=100) # Get more than we need to filter
425 |
426 | # Filter by minimum score
427 | qualifying_runs = [r for r in all_runs if r.get("score", 0) >= min_score]
428 |
429 | # Sort by score (highest first)
430 | qualifying_runs.sort(key=lambda r: r.get("score", 0), reverse=True)
431 |
432 | # Analyze the top runs
433 | return [self.analyze_run(r.get("run_id")) for r in qualifying_runs[:count]]
434 |
435 | def generate_report(self, run_id: str, format: str = "markdown") -> str:
436 | """
437 | Generate a comprehensive report for a run.
438 |
439 | Args:
440 | run_id: Run ID to generate report for
441 | format: Output format ('markdown', 'text', or 'html')
442 |
443 | Returns:
444 | Formatted report as a string
445 | """
446 | # Analyze the run
447 | analysis = self.analyze_run(run_id)
448 |
449 | if "error" in analysis:
450 | return f"Error: {analysis['error']}"
451 |
452 | # Get the run details for additional information
453 | run_details = self.get_run_details(run_id)
454 | if not run_details:
455 | return f"Error: Run details not found for {run_id}"
456 |
457 | # Generate the report based on the format
458 | if format == "markdown":
459 | return self._generate_markdown_report(analysis, run_details)
460 | elif format == "text":
461 | return self._generate_text_report(analysis, run_details)
462 | elif format == "html":
463 | return self._generate_html_report(analysis, run_details)
464 | else:
465 | return f"Unsupported format: {format}"
466 |
467 | def _generate_markdown_report(self, analysis: Dict[str, Any], run_details: Dict[str, Any]) -> str:
468 | """Generate a markdown report."""
469 | report = []
470 |
471 | # Header
472 | report.append(f"# MCTS Analysis Report: {analysis['run_id']}")
473 | report.append("")
474 |
475 | # Basic information
476 | report.append("## Basic Information")
477 | report.append("")
478 | report.append(f"- **Question:** {analysis['question']}")
479 | report.append(f"- **Model:** {analysis['model']}")
480 | report.append(f"- **Date:** {analysis['timestamp']}")
481 | report.append(f"- **Duration:** {analysis['duration']} seconds")
482 | report.append(f"- **Score:** {analysis['best_score']}")
483 | if analysis['tags']:
484 | report.append(f"- **Tags:** {', '.join(analysis['tags'])}")
485 | report.append("")
486 |
487 | # Key concepts
488 | if analysis.get('key_concepts'):
489 | report.append("## Key Concepts")
490 | report.append("")
491 | for concept in analysis['key_concepts']:
492 | report.append(f"- {concept}")
493 | report.append("")
494 |
495 | # Key arguments
496 | if analysis.get('arguments_for') or analysis.get('arguments_against'):
497 | report.append("## Key Arguments")
498 | report.append("")
499 |
500 | if analysis.get('arguments_for'):
501 | report.append("### Arguments For")
502 | report.append("")
503 | for arg in analysis['arguments_for']:
504 | report.append(f"- {arg}")
505 | report.append("")
506 |
507 | if analysis.get('arguments_against'):
508 | report.append("### Arguments Against")
509 | report.append("")
510 | for arg in analysis['arguments_against']:
511 | report.append(f"- {arg}")
512 | report.append("")
513 |
514 | # Conclusions
515 | if analysis.get('conclusions'):
516 | report.append("## Key Conclusions")
517 | report.append("")
518 | for conclusion in analysis['conclusions']:
519 | report.append(f"> {conclusion}")
520 | report.append("")
521 |
522 | # Best solution
523 | best_solution = run_details.get('best_solution', '')
524 | if best_solution:
525 | report.append("## Best Solution")
526 | report.append("")
527 | report.append("```")
528 | report.append(best_solution)
529 | report.append("```")
530 |
531 | return "\n".join(report)
532 |
533 | def _generate_text_report(self, analysis: Dict[str, Any], run_details: Dict[str, Any]) -> str:
534 | """Generate a plain text report."""
535 | report = []
536 |
537 | # Header
538 | report.append(f"MCTS Analysis Report: {analysis['run_id']}")
539 | report.append("=" * 80)
540 | report.append("")
541 |
542 | # Basic information
543 | report.append("Basic Information:")
544 | report.append(f" Question: {analysis['question']}")
545 | report.append(f" Model: {analysis['model']}")
546 | report.append(f" Date: {analysis['timestamp']}")
547 | report.append(f" Duration: {analysis['duration']} seconds")
548 | report.append(f" Score: {analysis['best_score']}")
549 | if analysis['tags']:
550 | report.append(f" Tags: {', '.join(analysis['tags'])}")
551 | report.append("")
552 |
553 | # Key concepts
554 | if analysis.get('key_concepts'):
555 | report.append("Key Concepts:")
556 | for concept in analysis['key_concepts']:
557 | report.append(f" * {concept}")
558 | report.append("")
559 |
560 | # Key arguments
561 | if analysis.get('arguments_for') or analysis.get('arguments_against'):
562 | report.append("Key Arguments:")
563 |
564 | if analysis.get('arguments_for'):
565 | report.append(" Arguments For:")
566 | for arg in analysis['arguments_for']:
567 | report.append(f" * {arg}")
568 | report.append("")
569 |
570 | if analysis.get('arguments_against'):
571 | report.append(" Arguments Against:")
572 | for arg in analysis['arguments_against']:
573 | report.append(f" * {arg}")
574 | report.append("")
575 |
576 | # Conclusions
577 | if analysis.get('conclusions'):
578 | report.append("Key Conclusions:")
579 | for conclusion in analysis['conclusions']:
580 | report.append(f" {conclusion}")
581 | report.append("")
582 |
583 | # Best solution
584 | best_solution = run_details.get('best_solution', '')
585 | if best_solution:
586 | report.append("Best Solution:")
587 | report.append("-" * 80)
588 | report.append(best_solution)
589 | report.append("-" * 80)
590 |
591 | return "\n".join(report)
592 |
593 | def _generate_html_report(self, analysis: Dict[str, Any], run_details: Dict[str, Any]) -> str:
594 | """Generate an HTML report."""
595 | # For now, we'll convert the markdown to basic HTML
596 | md_report = self._generate_markdown_report(analysis, run_details)
597 |
598 | # Convert headers
599 | html = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', md_report, flags=re.MULTILINE)
600 | html = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
601 | html = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
602 |
603 | # Convert lists
604 | html = re.sub(r'^- (.*?)$', r'<li>\1</li>', html, flags=re.MULTILINE)
605 | html = re.sub(r'(<li>.*?</li>\n)+', r'<ul>\n\g<0></ul>', html, flags=re.DOTALL)
606 |
607 | # Convert blockquotes
608 | html = re.sub(r'^> (.*?)$', r'<blockquote>\1</blockquote>', html, flags=re.MULTILINE)
609 |
610 | # Convert code blocks
611 | html = re.sub(r'```\n(.*?)```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
612 |
613 | # Convert line breaks
614 | html = re.sub(r'\n\n', r'<br><br>', html)
615 |
616 | # Wrap in basic HTML structure
617 | html = f"""<!DOCTYPE html>
618 | <html>
619 | <head>
620 | <meta charset="UTF-8">
621 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
622 | <title>MCTS Analysis Report: {analysis['run_id']}</title>
623 | <style>
624 | body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
625 | h1, h2, h3 {{ color: #333; }}
626 | blockquote {{ background-color: #f9f9f9; border-left: 5px solid #ccc; padding: 10px 20px; margin: 20px 0; }}
627 | pre {{ background-color: #f5f5f5; padding: 15px; overflow-x: auto; }}
628 | ul {{ margin-bottom: 20px; }}
629 | </style>
630 | </head>
631 | <body>
632 | {html}
633 | </body>
634 | </html>
635 | """
636 |
637 | return html
638 |
639 | def extract_insights(self, run_id: str, max_insights: int = 5) -> List[str]:
640 | """
641 | Extract key insights from a run's results.
642 |
643 | Args:
644 | run_id: The run ID to analyze
645 | max_insights: Maximum number of insights to extract
646 |
647 | Returns:
648 | List of key insights as strings
649 | """
650 | # Analyze the run
651 | analysis = self.analyze_run(run_id)
652 |
653 | if "error" in analysis:
654 | return [f"Error: {analysis['error']}"]
655 |
656 | insights = []
657 |
658 | # Add conclusions as insights
659 | for conclusion in analysis.get('conclusions', [])[:max_insights]:
660 | if conclusion and not any(self._text_similarity(conclusion, i) > 0.7 for i in insights):
661 | insights.append(conclusion)
662 |
663 | # Add key arguments as insights if we need more
664 | if len(insights) < max_insights:
665 | for_args = analysis.get('arguments_for', [])
666 | against_args = analysis.get('arguments_against', [])
667 |
668 | # Interleave arguments for and against
669 | all_args = []
670 | for i in range(max(len(for_args), len(against_args))):
671 | if i < len(for_args):
672 | all_args.append(("For: " + for_args[i]) if for_args[i].startswith("For: ") else for_args[i])
673 | if i < len(against_args):
674 | all_args.append(("Against: " + against_args[i]) if against_args[i].startswith("Against: ") else against_args[i])
675 |
676 | # Add arguments as insights
677 | for arg in all_args:
678 | if len(insights) >= max_insights:
679 | break
680 | if not any(self._text_similarity(arg, i) > 0.7 for i in insights):
681 | insights.append(arg)
682 |
683 | # Add key concepts as insights if we still need more
684 | if len(insights) < max_insights:
685 | for concept in analysis.get('key_concepts', []):
686 | if len(insights) >= max_insights:
687 | break
688 | if not any(self._text_similarity(concept, i) > 0.7 for i in insights):
689 | insights.append(concept)
690 |
691 | return insights
692 |
693 | def suggest_improvements(self, run_id: str) -> List[str]:
694 | """
695 | Suggest improvements for MCTS runs based on analysis.
696 |
697 | Args:
698 | run_id: The run ID to analyze
699 |
700 | Returns:
701 | List of improvement suggestions
702 | """
703 | # Analyze the run
704 | analysis = self.analyze_run(run_id)
705 |
706 | if "error" in analysis:
707 | return [f"Error: {analysis['error']}"]
708 |
709 | suggestions = []
710 |
711 | # Check if we've got enough iterations
712 | iterations = analysis.get('iterations', 0)
713 | if iterations < 2:
714 | suggestions.append(f"Increase iterations from {iterations} to at least 2 for more thorough exploration")
715 |
716 | # Check score
717 | score = analysis.get('best_score', 0)
718 | if score < 7.0:
719 | suggestions.append(f"Current score is {score}, which is relatively low. Try using a more sophisticated model or adjusting exploration parameters")
720 |
721 | # Check for diverse approaches
722 | if len(analysis.get('key_concepts', [])) < 3:
723 | suggestions.append("Limited key concepts identified. Consider increasing exploration weight parameter for more diverse thinking")
724 |
725 | # Check for balanced arguments
726 | if len(analysis.get('arguments_for', [])) > 0 and len(analysis.get('arguments_against', [])) == 0:
727 | suggestions.append("Arguments are one-sided (only 'for' arguments). Consider using a balanced prompt approach to get both sides")
728 | elif len(analysis.get('arguments_against', [])) > 0 and len(analysis.get('arguments_for', [])) == 0:
729 | suggestions.append("Arguments are one-sided (only 'against' arguments). Consider using a balanced prompt approach to get both sides")
730 |
731 | # Check for bayesian parameters if score is low
732 | if score < 8.0:
733 | suggestions.append("Try adjusting the prior parameters (beta_prior_alpha/beta) to improve the bandit algorithm performance")
734 |
735 | # Default suggestion
736 | if not suggestions:
737 | suggestions.append("The MCTS run looks good and achieved a reasonable score. For even better results, try increasing iterations or using a more capable model")
738 |
739 | return suggestions
740 |
```