From 0faa7775da0765d6e8e1ff7bb85f62d526f093a7 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 16 Aug 2025 16:17:25 +0000 Subject: [PATCH] Fix issue #52: Add a new speaker to the panel --- .env.template | 4 +- config.py | 3 +- llm/lambda_client.py | 67 ++++++++++++++++++++++++++++++++++ main.py | 7 +++- moderator/turn_manager.py | 2 +- tests/test_basic.py | 8 +++- tests/test_real_integration.py | 34 +++++++++++++++-- ui/terminal.py | 1 + 8 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 llm/lambda_client.py diff --git a/.env.template b/.env.template index 4866554..2bf6ca4 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,6 @@ # Rename this file to .env and add your API keys ANTHROPIC_API_KEY=your_anthropic_api_key_here OPENAI_API_KEY=your_openai_api_key_here -GOOGLE_API_KEY=your_google_api_key_here \ No newline at end of file +GOOGLE_API_KEY=your_google_api_key_here +LAMBDA_API_KEY=your_lambda_api_key_here + diff --git a/config.py b/config.py index c35e5ce..1d9f074 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,8 @@ API_KEYS = { "anthropic": os.getenv("ANTHROPIC_API_KEY"), "openai": os.getenv("OPENAI_API_KEY"), - "google": os.getenv("GOOGLE_API_KEY") + "google": os.getenv("GOOGLE_API_KEY"), + "lambda": os.getenv("LAMBDA_API_KEY"), } # Print diagnostic info diff --git a/llm/lambda_client.py b/llm/lambda_client.py new file mode 100644 index 0000000..ed3704c --- /dev/null +++ b/llm/lambda_client.py @@ -0,0 +1,67 @@ +try: + import openai + OPENAI_AVAILABLE = True +except ImportError: + print("OpenAI library not installed. Install with: pip install openai") + OPENAI_AVAILABLE = False + +from typing import List, Dict +from llm.base import LLMClient, retry_with_backoff +import asyncio +import logging + +class LambdaClient(LLMClient): + def __init__(self, api_key: str): + if not OPENAI_AVAILABLE: + raise ImportError("OpenAI library not installed. Run: pip install openai") + + if not api_key or api_key == "your_lambda_api_key_here": + raise ValueError("Invalid Lambda API key. Please check your .env file") + + try: + # Use OpenAI-compatible client with custom base_url + self.client = openai.OpenAI(api_key=api_key, base_url="https://api.lambda.ai/v1") + logging.info("Lambda (OpenAI-compatible) client initialized successfully") + except Exception as e: + logging.error(f"Failed to initialize Lambda client: {e}") + raise + + async def generate_response( + self, + system_prompt: str, + messages: List[Dict], + temperature: float = 0.7, + max_tokens: int = 2048 + ) -> str: + async def _generate(): + try: + messages_formatted = [{"role": "system", "content": system_prompt}] + messages + + # For OpenAI-compatible chat.completions, use max_tokens + response = await asyncio.to_thread( + self.client.chat.completions.create, + model="deepseek-llama3.3-70b", + messages=messages_formatted, + temperature=temperature, + max_tokens=max_tokens + ) + + if response and getattr(response, 'choices', None): + first = response.choices[0] + # Some providers may return "message" or "delta" + if hasattr(first, 'message') and first.message: + return first.message.content + elif hasattr(first, 'text') and first.text: + return first.text + else: + raise ValueError("Unexpected response format from Lambda API") + else: + raise ValueError("Empty response from Lambda API") + except openai.APIError as e: + logging.error(f"Lambda API (OpenAI-compatible) error: {e}") + raise + except Exception as e: + logging.error(f"Unexpected error calling Lambda API: {e}") + raise + + return await retry_with_backoff(_generate) diff --git a/main.py b/main.py index d5fa51d..2f11af6 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from llm.anthropic_client import ClaudeClient from llm.openai_client import GPTClient from llm.google_client import GeminiClient +from llm.lambda_client import LambdaClient from moderator.turn_manager import TurnManager from ui.terminal import TerminalUI from storage.session_logger import SessionLogger @@ -29,7 +30,8 @@ def __init__(self): "claude_moderator": ClaudeClient(API_KEYS["anthropic"]), "claude": ClaudeClient(API_KEYS["anthropic"]), "gpt5": GPTClient(API_KEYS["openai"]), - "gemini": GeminiClient(API_KEYS["google"]) + "gemini": GeminiClient(API_KEYS["google"]), + "deepseek": LambdaClient(API_KEYS["lambda"]), } except Exception as e: self.ui.console.print(f"[red]Error initializing LLM clients: {e}[/red]") @@ -40,7 +42,8 @@ def __init__(self): "claude_moderator": "Claude 4.1 Opus", "claude": "Claude 4.1 Opus", "gpt5": "GPT-5 Thinking", - "gemini": "Gemini 2.5 Pro" + "gemini": "Gemini 2.5 Pro", + "deepseek": "DeepSeek Llama3.3 70B (Lambda)", } self.current_session_file = None diff --git a/moderator/turn_manager.py b/moderator/turn_manager.py index 01e7bde..dbc4fc0 100644 --- a/moderator/turn_manager.py +++ b/moderator/turn_manager.py @@ -4,7 +4,7 @@ class TurnManager: def __init__(self): - self.panelist_ids = ["gpt5", "claude", "gemini"] + self.panelist_ids = ["gpt5", "claude", "gemini", "deepseek"] self.moderator_id = "claude_moderator" def determine_next_speaker(self, state: DiscussionState) -> str: diff --git a/tests/test_basic.py b/tests/test_basic.py index 6744dd0..1bb596d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -88,6 +88,8 @@ def test_turn_manager_initialization(): assert "claude" in manager.panelist_ids assert "gemini" in manager.panelist_ids + assert "deepseek" in manager.panelist_ids + def test_turn_manager_agenda_speaker(): """Test that moderator speaks first in agenda round""" from moderator.turn_manager import TurnManager @@ -218,6 +220,7 @@ def test_llm_client_initialization_mocked(mock_gemini_model, mock_gemini_config, from llm.anthropic_client import ClaudeClient from llm.openai_client import GPTClient from llm.google_client import GeminiClient + from llm.lambda_client import LambdaClient # These should not raise errors with valid keys claude = ClaudeClient("sk-ant-api03-valid-key-for-testing") @@ -229,6 +232,9 @@ def test_llm_client_initialization_mocked(mock_gemini_model, mock_gemini_config, gemini = GeminiClient("AIza-valid-key-for-testing") assert gemini.model is not None + deepseek = LambdaClient("lambda-valid-key-for-testing") + assert deepseek.client is not None + def test_config_loading(): """Test configuration loading""" import os @@ -247,4 +253,4 @@ def test_config_loading(): assert config.API_KEYS['anthropic'] == 'test_anthropic' assert config.API_KEYS['openai'] == 'test_openai' - assert config.API_KEYS['google'] == 'test_google' \ No newline at end of file + assert config.API_KEYS['google'] == 'test_google' diff --git a/tests/test_real_integration.py b/tests/test_real_integration.py index d983dd1..ad45152 100644 --- a/tests/test_real_integration.py +++ b/tests/test_real_integration.py @@ -5,6 +5,8 @@ - ANTHROPIC_API_KEY - OPENAI_API_KEY - GOOGLE_API_KEY +- LAMBDA_API_KEY + Set SKIP_REAL_TESTS=1 to skip these tests if API keys are not available. """ @@ -38,7 +40,8 @@ API_KEYS = { "anthropic": os.getenv("ANTHROPIC_API_KEY"), "openai": os.getenv("OPENAI_API_KEY"), - "google": os.getenv("GOOGLE_API_KEY") + "google": os.getenv("GOOGLE_API_KEY"), + "lambda": os.getenv("LAMBDA_API_KEY"), } # Configure logging for tests @@ -52,7 +55,7 @@ def pytest_configure(config): def check_api_keys_available(): """Check if real API keys are available for testing""" - required_keys = ["anthropic", "openai", "google"] + required_keys = ["anthropic", "openai", "google", "lambda"] missing_keys = [] for key in required_keys: @@ -111,6 +114,13 @@ def test_real_google_client_initialization(self): assert client.model is not None assert hasattr(client.model, 'generate_content') + def test_real_lambda_client_initialization(self): + """Test real Lambda client initialization with actual API key""" + from llm.lambda_client import LambdaClient + client = LambdaClient(API_KEYS["lambda"]) + assert client.client is not None + assert hasattr(client.client, 'chat') + @pytest.mark.asyncio async def test_real_anthropic_generate_response(self): """Test real Anthropic API call with simple prompt""" @@ -162,6 +172,24 @@ async def test_real_google_generate_response(self): system_prompt = "You are a helpful assistant. Respond briefly." messages = [{"role": "user", "content": "Say hello in exactly 3 words."}] + + @pytest.mark.asyncio + async def test_real_lambda_generate_response(self): + """Test real Lambda API call with simple prompt""" + from llm.lambda_client import LambdaClient + client = LambdaClient(API_KEYS["lambda"]) + system_prompt = "You are a helpful assistant. Respond briefly." + messages = [{"role": "user", "content": "Say hello in exactly 3 words."}] + response = await client.generate_response( + system_prompt=system_prompt, + messages=messages, + temperature=0.1, + max_tokens=128, + ) + assert isinstance(response, str) + assert len(response.strip()) > 0 + assert len(response.split()) <= 10 + response = await client.generate_response( system_prompt=system_prompt, messages=messages, @@ -291,4 +319,4 @@ def test_skip_conditions(): # This should always pass assert isinstance(skip, bool) - assert isinstance(reason, (str, type(None))) \ No newline at end of file + assert isinstance(reason, (str, type(None))) diff --git a/ui/terminal.py b/ui/terminal.py index 073a09d..cf1b802 100644 --- a/ui/terminal.py +++ b/ui/terminal.py @@ -24,6 +24,7 @@ def __init__(self): "gpt5": "cyan", "claude": "green", "gemini": "yellow", + "deepseek": "blue", "claude_moderator": "magenta" }