diff --git a/app/providers/__init__.py b/app/providers/__init__.py index 7af7283..b19d3ae 100644 --- a/app/providers/__init__.py +++ b/app/providers/__init__.py @@ -6,7 +6,7 @@ from .mistral_client import MistralAI, ChatMistralAI, MistralAgent, ChatMistralAgent from .gemini_client import ChatGemini, GeminiAgent from .groq_client import ChatGroq, GroqAgent -from .openrouter_client import ChatOpenRouter +from .openrouter_client import ChatOpenRouter, OpenRouterAgent, ChatOpenRouterAgent __all__ = [ "BaseChatModel", @@ -19,7 +19,9 @@ "GeminiAgent", "ChatGroq", "GroqAgent", - "ChatOpenRouter" + "ChatOpenRouter", + "OpenRouterAgent", + "ChatOpenRouterAgent" ] """Parameters for Each Function Call diff --git a/app/providers/openrouter_client.py b/app/providers/openrouter_client.py index 7458203..7b40d29 100644 --- a/app/providers/openrouter_client.py +++ b/app/providers/openrouter_client.py @@ -6,6 +6,7 @@ from app.Errors import * from typing import Any, Dict, List, Optional from langchain_core.messages import AIMessage +from langchain_core.tools import tool, StructuredTool import warnings from app.providers.base import BaseChatModel, BaseAgentModel from app.defaults import DEFAULT_OPENROUTER_MODEL, MAX_TOKENS, TEMPERATURE @@ -50,8 +51,8 @@ def __init__( model : str = DEFAULT_OPENROUTER_MODEL, temperature : float = TEMPERATURE, max_tokens : int = MAX_TOKENS, - app_title : str = "RSRoute Server", - app_url : str = "https://www.github.com/ItzRustam/rsroute", + app_title : str = "RSRoute", + app_url : str = "https://www.rsroute.freedev.app/", app_categories : List[str] = None, max_retries : int = 2, @@ -95,3 +96,116 @@ def __init__( # Backwards compatibility attributes self.__model = self._model self.__params = self._params + + +class OpenRouterAgent(BaseAgentModel): + """OpenRouter-powered agentic wrapper with tool-binding support via Langchain. + + Wraps `ChatOpenRouter` and binds fake/stub tools to the model, + enabling the model to reason about and invoke external tools by name. + + Attributes: + _model: Instance of Langchain's `ChatOpenRouter` with tools bound. + _params (Dict[str, Any]): Dictionary containing the parameters used to initialize + the model, useful for debugging or introspection. + + Raises: + InvalidRequest: If `api_key` is not provided during initialization. + InvalidRequest: If `tools_info` is not provided, empty, or incorrectly formatted. + + Example: + ```python + tools = [{"name": "web_search", "description": "Search the web"}] + agent = OpenRouterAgent( + api_key="sk-or-v1-FAKE_KEY", + tools_info=tools + ) + response = agent("What is the weather today?") + ``` + """ + + def __fake_tool(self, name : str, description : str) -> StructuredTool: + return self._fake_tool(name=name, description=description) + + def __create_fake_tools(self, tools_info : List[Dict[str, str]]) -> List[StructuredTool]: + return self._create_fake_tools(tools_info=tools_info) + + def __init__( + self, + api_key : str = None, + model : str = DEFAULT_OPENROUTER_MODEL, + temperature : float = TEMPERATURE, + max_tokens : int = MAX_TOKENS, + app_title : str = "RSRoute", + app_url : str = "https://www.rsroute.freedev.app/", + app_categories : List[str] = None, + max_retries : int = 2, + + # provider preferences + providers : Dict[Any, Any] = None, + + # Base url + end_point : str = None, + + tools_info : List[Dict[str, str]] = None, + **model_kwargs + ) -> AIMessage: + warnings.warn("For Best Agentic Experience use rsroute python package. https://www.github.com/ItzRustam/rsroute-package") + super().__init__() + self._validate_api_key(api_key=api_key) + self._validate_tools_info(tools_info) + + created_tools = self._create_fake_tools(tools_info=tools_info) + + # Parameter Holding + self._params : Dict[str, Any] = { + "model" : model, + "temperature" : temperature, + "max_tokens" : max_tokens, + "app_url" : app_url, + "app_title" : app_title, + "app_categories" : app_categories, + "max_retries" : max_retries, + "openrouter_provider" : providers, + "end_point" : end_point, + "binded_tools" : tools_info, + "model_kwargs" : model_kwargs + } + + # Hidden Agent Model + self._model = _LangchainChatOpenRouter( + model=model, + api_key=api_key, + temperature=temperature, + max_tokens=max_tokens, + base_url=end_point, + app_title=app_title, + app_url=app_url, + app_categories=app_categories, + max_retries=max_retries, + openrouter_provider=providers, + model_kwargs=model_kwargs + ).bind_tools(tools=created_tools) + + self.__agent = self._model + self.__params = self._params + + def __call__(self, query : Any = None): + """Generate response on given `query`. Same as `generate()`.""" + return self.generate(query=query) + + def generate(self, query : Any = None): + """Generate response on given `query`.""" + return super().generate(query=query) + + def get_params(self): + """Model initialized parameters.""" + return super().get_params() + + +ChatOpenRouterAgent = OpenRouterAgent + + +if __name__ == "__main__": + llm = ChatOpenRouter(model="openai/gpt-4o-mini", api_key="sk-or-v1-fake_key") + response = llm(query="Hello? How can you help me?") diff --git a/app/server/root.py b/app/server/root.py index 0185934..813f8f8 100644 --- a/app/server/root.py +++ b/app/server/root.py @@ -47,6 +47,7 @@ def root(): }, "NOTE": "use exact `master_key` given in `.env` or else it won't work. don't forget to put API key which model you wanna use.", "GitHub": "https://www.github.com/ItzRustam/rsroute", + "rsroute": "https://www.rsroute.freedev.app/", "By": "ItzRustam (Rustam Bhadouriya)", "version": "v1-starter-version", } diff --git a/test_runner.py b/test_runner.py index fa46818..ab530c1 100644 --- a/test_runner.py +++ b/test_runner.py @@ -8,9 +8,10 @@ from tests import auth_test from tests import get_logger from tests import api_config_test -from tests import test_mistral +from tests import test_mistral, test_mistral_agent from tests import test_gemini, test_gemini_agent -from tests import test_groq +from tests import test_groq, test_groq_agent +from tests import test_openrouter, test_openrouter_agent from tests import ( test_base_estimator_inheritance, test_base_estimator_methods, @@ -29,6 +30,48 @@ case_tools_missing_name, case_tools_empty_dict_in_list, ) +from tests.groq_agent_test import ( + case_tools_none as groq_case_tools_none, + case_tools_empty_list as groq_case_tools_empty_list, + case_tools_wrong_type_string as groq_case_tools_wrong_type_string, + case_tools_wrong_type_int as groq_case_tools_wrong_type_int, + case_tools_wrong_type_dict as groq_case_tools_wrong_type_dict, + case_tools_missing_description as groq_case_tools_missing_description, + case_tools_missing_name as groq_case_tools_missing_name, + case_tools_empty_dict_in_list as groq_case_tools_empty_dict_in_list, + case_api_key_none as groq_case_api_key_none, + case_valid_initialization as groq_case_valid_initialization, + case_agent_inheritance as groq_case_agent_inheritance, + case_agent_methods as groq_case_agent_methods, +) +from tests.mistral_agent_test import ( + case_tools_none as mistral_case_tools_none, + case_tools_empty_list as mistral_case_tools_empty_list, + case_tools_wrong_type_string as mistral_case_tools_wrong_type_string, + case_tools_wrong_type_int as mistral_case_tools_wrong_type_int, + case_tools_wrong_type_dict as mistral_case_tools_wrong_type_dict, + case_tools_missing_description as mistral_case_tools_missing_description, + case_tools_missing_name as mistral_case_tools_missing_name, + case_tools_empty_dict_in_list as mistral_case_tools_empty_dict_in_list, + case_api_key_none as mistral_case_api_key_none, + case_valid_initialization as mistral_case_valid_initialization, + case_agent_inheritance as mistral_case_agent_inheritance, + case_agent_methods as mistral_case_agent_methods, +) +from tests.openrouter_agent_test import ( + case_tools_none as or_case_tools_none, + case_tools_empty_list as or_case_tools_empty_list, + case_tools_wrong_type_string as or_case_tools_wrong_type_string, + case_tools_wrong_type_int as or_case_tools_wrong_type_int, + case_tools_wrong_type_dict as or_case_tools_wrong_type_dict, + case_tools_missing_description as or_case_tools_missing_description, + case_tools_missing_name as or_case_tools_missing_name, + case_tools_empty_dict_in_list as or_case_tools_empty_dict_in_list, + case_api_key_none as or_case_api_key_none, + case_valid_initialization as or_case_valid_initialization, + case_agent_inheritance as or_case_agent_inheritance, + case_agent_methods as or_case_agent_methods, +) from dotenv import load_dotenv load_dotenv() @@ -283,12 +326,147 @@ def test_gemini_agent(): return True +def test_groq_agent_cases(): + print("=========== Running GroqAgent tools_info TestCases ===========") + + cases = [ + ("TestCase #1 - tools_info=None", groq_case_tools_none), + ("TestCase #2 - tools_info=[] (empty list)", groq_case_tools_empty_list), + ("TestCase #3 - tools_info='search' (string)", groq_case_tools_wrong_type_string), + ("TestCase #4 - tools_info=42 (int)", groq_case_tools_wrong_type_int), + ("TestCase #5 - tools_info={} (bare dict)", groq_case_tools_wrong_type_dict), + ("TestCase #6 - missing 'description' key", groq_case_tools_missing_description), + ("TestCase #7 - missing 'name' key", groq_case_tools_missing_name), + ("TestCase #8 - [{}] empty dict in list", groq_case_tools_empty_dict_in_list), + ("TestCase #9 - api_key=None", groq_case_api_key_none), + ("TestCase #10 - Valid initialization & params", groq_case_valid_initialization), + ("TestCase #11 - GroqAgent inheritance", groq_case_agent_inheritance), + ("TestCase #12 - GroqAgent methods", groq_case_agent_methods), + ] + + passed = 0 + for label, case_fn in cases: + logger.info(label) + val, result = case_fn() + logger.debug(f"Result: {result}") + if val: + passed += 1 + else: + logger.error(f"{label} — FAILED: {result}") + return False + + print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========") + return True + + +def test_mistral_agent_cases(): + print("=========== Running MistralAgent tools_info TestCases ===========") + + cases = [ + ("TestCase #1 - tools_info=None", mistral_case_tools_none), + ("TestCase #2 - tools_info=[] (empty list)", mistral_case_tools_empty_list), + ("TestCase #3 - tools_info='search' (string)", mistral_case_tools_wrong_type_string), + ("TestCase #4 - tools_info=42 (int)", mistral_case_tools_wrong_type_int), + ("TestCase #5 - tools_info={} (bare dict)", mistral_case_tools_wrong_type_dict), + ("TestCase #6 - missing 'description' key", mistral_case_tools_missing_description), + ("TestCase #7 - missing 'name' key", mistral_case_tools_missing_name), + ("TestCase #8 - [{}] empty dict in list", mistral_case_tools_empty_dict_in_list), + ("TestCase #9 - api_key=None", mistral_case_api_key_none), + ("TestCase #10 - Valid initialization & params", mistral_case_valid_initialization), + ("TestCase #11 - MistralAgent inheritance", mistral_case_agent_inheritance), + ("TestCase #12 - MistralAgent methods", mistral_case_agent_methods), + ] + + passed = 0 + for label, case_fn in cases: + logger.info(label) + val, result = case_fn() + logger.debug(f"Result: {result}") + if val: + passed += 1 + else: + logger.error(f"{label} — FAILED: {result}") + return False + + print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========") + return True + + +def openrouter_testing(): + FAKE_KEY = "sk-or-v1-fake_openrouter_key" + print("=========== Running ChatOpenRouter TestCases with Fake API ===========") + + logger.info("TestCase #1 - KEY & query both None.") + val, result = test_openrouter() + logger.debug(f"case #1 result - {result}") + if not val: + logger.error("TestCase #1 Failed.") + return False + + logger.info("TestCase #2 - query = None") + val, result = test_openrouter(KEY=FAKE_KEY) + logger.debug(f"case #2 result - {result}") + if not val: + logger.error("TestCase #2 Failed.") + return False + + logger.info("TestCase #3 - valid value with real_api=False") + val, result = test_openrouter(KEY=FAKE_KEY, query="hey", real_api=False) + logger.debug(f"case #3 result - {result}") + if not val: + logger.error("TestCase #3 Failed.") + return False + + logger.info("TestCase #4 - valid value with real_api=True with fake api") + val, result = test_openrouter(KEY=FAKE_KEY, query="hey", real_api=True) + logger.debug(f"case #4 result - {result}") + if val != False: + logger.error("TestCase #4 Failed.") + return False + + print("=========== All Test Case Passed 4/4 ===========") + return True + + +def test_openrouter_agent_cases(): + print("=========== Running OpenRouterAgent tools_info TestCases ===========") + + cases = [ + ("TestCase #1 - tools_info=None", or_case_tools_none), + ("TestCase #2 - tools_info=[] (empty list)", or_case_tools_empty_list), + ("TestCase #3 - tools_info='search' (string)", or_case_tools_wrong_type_string), + ("TestCase #4 - tools_info=42 (int)", or_case_tools_wrong_type_int), + ("TestCase #5 - tools_info={} (bare dict)", or_case_tools_wrong_type_dict), + ("TestCase #6 - missing 'description' key", or_case_tools_missing_description), + ("TestCase #7 - missing 'name' key", or_case_tools_missing_name), + ("TestCase #8 - [{}] empty dict in list", or_case_tools_empty_dict_in_list), + ("TestCase #9 - api_key=None", or_case_api_key_none), + ("TestCase #10 - Valid initialization & params", or_case_valid_initialization), + ("TestCase #11 - OpenRouterAgent inheritance", or_case_agent_inheritance), + ("TestCase #12 - OpenRouterAgent methods", or_case_agent_methods), + ] + + passed = 0 + for label, case_fn in cases: + logger.info(label) + val, result = case_fn() + logger.debug(f"Result: {result}") + if val: + passed += 1 + else: + logger.error(f"{label} — FAILED: {result}") + return False + + print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========") + return True + + def base_estimator_testing(): print("=========== Running BaseChatModel, Agent Classes & Endpoints TestCases ===========") cases = [ ("TestCase #1 - BaseChatModel & BaseAgentModel inheritance", test_base_estimator_inheritance), ("TestCase #2 - BaseChatModel validation & parameter methods", test_base_estimator_methods), - ("TestCase #3 - Agent classes tools_info validation (Gemini, Groq, Mistral)", test_agent_classes_validation), + ("TestCase #3 - Agent classes tools_info validation (Gemini, Groq, Mistral, OpenRouter)", test_agent_classes_validation), ("TestCase #4 - DDGS web_search tool check", test_ddgs_web_search), ("TestCase #5 - Groq chat & parameters endpoints (/v1/groq/chat, /v1/groq/parameters)", test_groq_endpoints), ("TestCase #6 - Agent endpoints (/v1/gemini/agent, /v1/groq/agent, /v1/mistral/agent)", test_agent_endpoints), @@ -316,7 +494,11 @@ def main(): results.append(mistral_testing()) # Mistral provider testing results.append(gemini_testing()) # Gemini Testing results.append(groq_testing()) # Groq Testing + results.append(openrouter_testing()) # OpenRouter Testing results.append(test_gemini_agent()) # GeminiAgent tools_info Testing + results.append(test_groq_agent_cases()) # GroqAgent tools_info Testing + results.append(test_mistral_agent_cases()) # MistralAgent tools_info Testing + results.append(test_openrouter_agent_cases()) # OpenRouterAgent tools_info Testing results.append(base_estimator_testing()) # BaseChatModel & Groq endpoints Testing return all(results) # Return False if any Test Case Failed diff --git a/tests/__init__.py b/tests/__init__.py index 5848989..bf7fec0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,6 +9,10 @@ from .gemini_client_test import test_gemini from .groq_client_test import test_groq from .gemini_agent_test import test_gemini_agent +from .groq_agent_test import test_groq_agent +from .mistral_agent_test import test_mistral_agent +from .openrouter_agent_test import test_openrouter_agent +from .openrouter_client_test import test_openrouter from .base_estimator_test import ( test_base_estimator_inheritance, test_base_estimator_methods, @@ -27,6 +31,10 @@ "test_gemini", "test_groq", "test_gemini_agent", + "test_groq_agent", + "test_mistral_agent", + "test_openrouter_agent", + "test_openrouter", "test_base_estimator_inheritance", "test_base_estimator_methods", "test_agent_classes_validation", diff --git a/tests/base_estimator_test.py b/tests/base_estimator_test.py index 6a11213..1d28665 100644 --- a/tests/base_estimator_test.py +++ b/tests/base_estimator_test.py @@ -11,9 +11,11 @@ ChatGemini, ChatMistralAI, MistralAI, + ChatOpenRouter, GeminiAgent, GroqAgent, MistralAgent, + OpenRouterAgent, ) from app.Errors import InvalidRequest from fastapi.testclient import TestClient @@ -28,6 +30,7 @@ def test_base_estimator_inheritance(): assert issubclass(ChatGemini, BaseChatModel), "ChatGemini must be a subclass of BaseChatModel" assert issubclass(ChatMistralAI, BaseChatModel), "ChatMistralAI must be a subclass of BaseChatModel" assert issubclass(MistralAI, BaseChatModel), "MistralAI must be a subclass of BaseChatModel" + assert issubclass(ChatOpenRouter, BaseChatModel), "ChatOpenRouter must be a subclass of BaseChatModel" assert ChatMistralAI is MistralAI, "ChatMistralAI should alias MistralAI" # BaseAgentModel inheritance @@ -35,6 +38,7 @@ def test_base_estimator_inheritance(): assert issubclass(GeminiAgent, BaseAgentModel), "GeminiAgent must be a subclass of BaseAgentModel" assert issubclass(GroqAgent, BaseAgentModel), "GroqAgent must be a subclass of BaseAgentModel" assert issubclass(MistralAgent, BaseAgentModel), "MistralAgent must be a subclass of BaseAgentModel" + assert issubclass(OpenRouterAgent, BaseAgentModel), "OpenRouterAgent must be a subclass of BaseAgentModel" return True, "All provider and agent classes inherit from BaseChatModel / BaseAgentModel" @@ -42,7 +46,7 @@ def test_base_estimator_inheritance(): def test_base_estimator_methods(): """Verify BaseChatModel parameter and query validation behavior.""" # Test api_key=None raises InvalidRequest - for cls in [ChatGroq, ChatGemini, ChatMistralAI]: + for cls in [ChatGroq, ChatGemini, ChatMistralAI, ChatOpenRouter]: try: cls(api_key=None) return False, f"{cls.__name__} should raise InvalidRequest when api_key=None" @@ -63,7 +67,7 @@ def test_agent_classes_validation(): tool = {"name": "web_search", "description": "Search the web"} # Test api_key=None - for agent_cls in [GeminiAgent, GroqAgent, MistralAgent]: + for agent_cls in [GeminiAgent, GroqAgent, MistralAgent, OpenRouterAgent]: try: agent_cls(api_key=None, tools_info=[tool]) return False, f"{agent_cls.__name__} should raise InvalidRequest when api_key=None" diff --git a/tests/groq_agent_test.py b/tests/groq_agent_test.py new file mode 100644 index 0000000..abc453b --- /dev/null +++ b/tests/groq_agent_test.py @@ -0,0 +1,118 @@ +# RSRoute +# Copyright (c) 2026 ItzRustam +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for GroqAgent tools_info validation, parameters, and inheritance. +NOTE: These tests only cover tools_info input validation and parameter holding — no real API key required. +""" + +from app.providers import GroqAgent +from app.providers.base import BaseAgentModel, BaseChatModel +from app.Errors import InvalidRequest + +FAKE_KEY = "my_groq_fake_key" + +def test_groq_agent(tools: list = None, api_key: str = FAKE_KEY): + """Run GroqAgent with given tools_info and return (passed, message). + + Returns: + (True, message) — InvalidRequest was raised as expected (validation caught it). + (False, message) — Unexpected exception or unexpected success. + """ + try: + agent = GroqAgent(api_key=api_key, tools_info=tools) + except InvalidRequest as IR: + return True, str(IR) # Expected — validation rejected bad input + except Exception as E: + return False, str(E) # Unexpected crash + else: + return False, "Agent initialised without error — should have raised InvalidRequest" + + +# --- Individual named cases --- + +def case_tools_none(): + """tools_info=None — should raise InvalidRequest.""" + return test_groq_agent(tools=None) + +def case_tools_empty_list(): + """tools_info=[] — empty list, should raise InvalidRequest.""" + return test_groq_agent(tools=[]) + +def case_tools_wrong_type_string(): + """tools_info='search' — a string instead of list, should raise InvalidRequest.""" + return test_groq_agent(tools="search") + +def case_tools_wrong_type_int(): + """tools_info=42 — an int instead of list, should raise InvalidRequest.""" + return test_groq_agent(tools=42) + +def case_tools_wrong_type_dict(): + """tools_info={} — a bare dict instead of list, should raise InvalidRequest.""" + return test_groq_agent(tools={}) + +def case_tools_missing_description(): + """tools_info=[{"name": "search"}] — missing 'description' key.""" + return test_groq_agent(tools=[{"name": "search"}]) + +def case_tools_missing_name(): + """tools_info=[{"description": "search the web"}] — missing 'name' key.""" + return test_groq_agent(tools=[{"description": "search the web"}]) + +def case_tools_empty_dict_in_list(): + """tools_info=[{}] — dict with no keys at all.""" + return test_groq_agent(tools=[{}]) + +def case_api_key_none(): + """api_key=None — should raise InvalidRequest.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + GroqAgent(api_key=None, tools_info=tool) + except InvalidRequest: + return True, "InvalidRequest raised as expected for api_key=None" + except Exception as E: + return False, str(E) + else: + return False, "Agent initialised without error when api_key=None" + +def case_valid_initialization(): + """Valid api_key and tools_info — should initialize and hold all parameters.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + agent = GroqAgent( + api_key=FAKE_KEY, + tools_info=tool, + model="openai/gpt-oss-120b", + temperature=0.5, + max_tokens=1000, + end_point="https://api.groq.com/openai/v1", + ) + params = agent.get_params() + assert params.get("model") == "openai/gpt-oss-120b", "model parameter mismatch" + assert params.get("temperature") == 0.5, "temperature parameter mismatch" + assert params.get("max_tokens") == 1000, "max_tokens parameter mismatch" + assert params.get("end_point") == "https://api.groq.com/openai/v1", "end_point parameter mismatch" + assert params.get("binded_tools") == tool, "binded_tools parameter mismatch" + return True, "Valid initialization and params check passed" + except Exception as E: + return False, str(E) + +def case_agent_inheritance(): + """Verify GroqAgent inherits from BaseAgentModel and BaseChatModel.""" + try: + assert issubclass(GroqAgent, BaseAgentModel), "Must inherit from BaseAgentModel" + assert issubclass(GroqAgent, BaseChatModel), "Must inherit from BaseChatModel" + return True, "GroqAgent inheritance check passed" + except Exception as E: + return False, str(E) + +def case_agent_methods(): + """Verify GroqAgent implements generate, get_params, and __call__.""" + try: + assert callable(getattr(GroqAgent, "generate", None)), "generate must be callable" + assert callable(getattr(GroqAgent, "get_params", None)), "get_params must be callable" + assert callable(getattr(GroqAgent, "__call__", None)), "__call__ must be callable" + return True, "GroqAgent methods check passed" + except Exception as E: + return False, str(E) + diff --git a/tests/mistral_agent_test.py b/tests/mistral_agent_test.py new file mode 100644 index 0000000..94985f1 --- /dev/null +++ b/tests/mistral_agent_test.py @@ -0,0 +1,120 @@ +# RSRoute +# Copyright (c) 2026 ItzRustam +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for MistralAgent tools_info validation, parameters, and inheritance. +NOTE: These tests only cover tools_info input validation and parameter holding — no real API key required. +""" + +from app.providers import MistralAgent +from app.providers.base import BaseAgentModel, BaseChatModel +from app.Errors import InvalidRequest + +FAKE_KEY = "my_mistral_fake_key" + +def test_mistral_agent(tools: list = None, api_key: str = FAKE_KEY): + """Run MistralAgent with given tools_info and return (passed, message). + + Returns: + (True, message) — InvalidRequest was raised as expected (validation caught it). + (False, message) — Unexpected exception or unexpected success. + """ + try: + agent = MistralAgent(api_key=api_key, tools_info=tools) + except InvalidRequest as IR: + return True, str(IR) # Expected — validation rejected bad input + except Exception as E: + return False, str(E) # Unexpected crash + else: + return False, "Agent initialised without error — should have raised InvalidRequest" + + +# --- Individual named cases --- + +def case_tools_none(): + """tools_info=None — should raise InvalidRequest.""" + return test_mistral_agent(tools=None) + +def case_tools_empty_list(): + """tools_info=[] — empty list, should raise InvalidRequest.""" + return test_mistral_agent(tools=[]) + +def case_tools_wrong_type_string(): + """tools_info='search' — a string instead of list, should raise InvalidRequest.""" + return test_mistral_agent(tools="search") + +def case_tools_wrong_type_int(): + """tools_info=42 — an int instead of list, should raise InvalidRequest.""" + return test_mistral_agent(tools=42) + +def case_tools_wrong_type_dict(): + """tools_info={} — a bare dict instead of list, should raise InvalidRequest.""" + return test_mistral_agent(tools={}) + +def case_tools_missing_description(): + """tools_info=[{"name": "search"}] — missing 'description' key.""" + return test_mistral_agent(tools=[{"name": "search"}]) + +def case_tools_missing_name(): + """tools_info=[{"description": "search the web"}] — missing 'name' key.""" + return test_mistral_agent(tools=[{"description": "search the web"}]) + +def case_tools_empty_dict_in_list(): + """tools_info=[{}] — dict with no keys at all.""" + return test_mistral_agent(tools=[{}]) + +def case_api_key_none(): + """api_key=None — should raise InvalidRequest.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + MistralAgent(api_key=None, tools_info=tool) + except InvalidRequest: + return True, "InvalidRequest raised as expected for api_key=None" + except Exception as E: + return False, str(E) + else: + return False, "Agent initialised without error when api_key=None" + +def case_valid_initialization(): + """Valid api_key and tools_info — should initialize and hold all parameters.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + agent = MistralAgent( + api_key=FAKE_KEY, + tools_info=tool, + model="mistral-small-2603", + temperature=0.5, + max_tokens=1000, + top_p=0.9, + end_point="https://api.mistral.ai", + ) + params = agent.get_params() + assert params.get("model") == "mistral-small-2603", "model parameter mismatch" + assert params.get("temperature") == 0.5, "temperature parameter mismatch" + assert params.get("max_tokens") == 1000, "max_tokens parameter mismatch" + assert params.get("top_p") == 0.9, "top_p parameter mismatch" + assert params.get("end_point") == "https://api.mistral.ai", "end_point parameter mismatch" + assert params.get("binded_tools") == tool, "binded_tools parameter mismatch" + return True, "Valid initialization and params check passed" + except Exception as E: + return False, str(E) + +def case_agent_inheritance(): + """Verify MistralAgent inherits from BaseAgentModel and BaseChatModel.""" + try: + assert issubclass(MistralAgent, BaseAgentModel), "Must inherit from BaseAgentModel" + assert issubclass(MistralAgent, BaseChatModel), "Must inherit from BaseChatModel" + return True, "MistralAgent inheritance check passed" + except Exception as E: + return False, str(E) + +def case_agent_methods(): + """Verify MistralAgent implements generate, get_params, and __call__.""" + try: + assert callable(getattr(MistralAgent, "generate", None)), "generate must be callable" + assert callable(getattr(MistralAgent, "get_params", None)), "get_params must be callable" + assert callable(getattr(MistralAgent, "__call__", None)), "__call__ must be callable" + return True, "MistralAgent methods check passed" + except Exception as E: + return False, str(E) + diff --git a/tests/openrouter_agent_test.py b/tests/openrouter_agent_test.py new file mode 100644 index 0000000..7f91d08 --- /dev/null +++ b/tests/openrouter_agent_test.py @@ -0,0 +1,130 @@ +# RSRoute +# Copyright (c) 2026 ItzRustam +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for OpenRouterAgent tools_info validation, parameters, and inheritance. +NOTE: These tests only cover tools_info input validation and parameter holding — no real API key required. +""" + +from app.providers import OpenRouterAgent +from app.providers.base import BaseAgentModel, BaseChatModel +from app.Errors import InvalidRequest + +FAKE_KEY = "my_openrouter_fake_key" + +def test_openrouter_agent(tools: list = None, api_key: str = FAKE_KEY): + """Run OpenRouterAgent with given tools_info and return (passed, message). + + Returns: + (True, message) — InvalidRequest was raised as expected (validation caught it). + (False, message) — Unexpected exception or unexpected success. + """ + try: + agent = OpenRouterAgent(api_key=api_key, tools_info=tools) + except InvalidRequest as IR: + return True, str(IR) # Expected — validation rejected bad input + except Exception as E: + return False, str(E) # Unexpected crash + else: + return False, "Agent initialised without error — should have raised InvalidRequest" + + +# --- Individual named cases --- + +def case_tools_none(): + """tools_info=None — should raise InvalidRequest.""" + return test_openrouter_agent(tools=None) + +def case_tools_empty_list(): + """tools_info=[] — empty list, should raise InvalidRequest.""" + return test_openrouter_agent(tools=[]) + +def case_tools_wrong_type_string(): + """tools_info='search' — a string instead of list, should raise InvalidRequest.""" + return test_openrouter_agent(tools="search") + +def case_tools_wrong_type_int(): + """tools_info=42 — an int instead of list, should raise InvalidRequest.""" + return test_openrouter_agent(tools=42) + +def case_tools_wrong_type_dict(): + """tools_info={} — a bare dict instead of list, should raise InvalidRequest.""" + return test_openrouter_agent(tools={}) + +def case_tools_missing_description(): + """tools_info=[{"name": "search"}] — missing 'description' key.""" + return test_openrouter_agent(tools=[{"name": "search"}]) + +def case_tools_missing_name(): + """tools_info=[{"description": "search the web"}] — missing 'name' key.""" + return test_openrouter_agent(tools=[{"description": "search the web"}]) + +def case_tools_empty_dict_in_list(): + """tools_info=[{}] — dict with no keys at all.""" + return test_openrouter_agent(tools=[{}]) + +def case_api_key_none(): + """api_key=None — should raise InvalidRequest.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + OpenRouterAgent(api_key=None, tools_info=tool) + except InvalidRequest: + return True, "InvalidRequest raised as expected for api_key=None" + except Exception as E: + return False, str(E) + else: + return False, "Agent initialised without error when api_key=None" + +def case_valid_initialization(): + """Valid api_key and tools_info — should initialize and hold all parameters.""" + tool = [{"name": "web_search", "description": "Search the web"}] + try: + agent = OpenRouterAgent( + api_key=FAKE_KEY, + tools_info=tool, + model="openai/gpt-4o-mini", + temperature=0.5, + max_tokens=1000, + app_title="TestApp", + app_url="https://test.app", + app_categories=["test"], + max_retries=3, + providers={"order": ["OpenAI"]}, + end_point="https://openrouter.ai/api/v1", + custom_arg="test_val", + ) + params = agent.get_params() + assert params.get("model") == "openai/gpt-4o-mini", "model parameter mismatch" + assert params.get("temperature") == 0.5, "temperature parameter mismatch" + assert params.get("max_tokens") == 1000, "max_tokens parameter mismatch" + assert params.get("app_title") == "TestApp", "app_title parameter mismatch" + assert params.get("app_url") == "https://test.app", "app_url parameter mismatch" + assert params.get("app_categories") == ["test"], "app_categories parameter mismatch" + assert params.get("max_retries") == 3, "max_retries parameter mismatch" + assert params.get("openrouter_provider") == {"order": ["OpenAI"]}, "openrouter_provider mismatch" + assert params.get("end_point") == "https://openrouter.ai/api/v1", "end_point parameter mismatch" + assert params.get("binded_tools") == tool, "binded_tools parameter mismatch" + assert params.get("model_kwargs") == {"custom_arg": "test_val"}, "model_kwargs parameter mismatch" + return True, "Valid initialization and params check passed" + except Exception as E: + return False, str(E) + +def case_agent_inheritance(): + """Verify OpenRouterAgent inherits from BaseAgentModel and BaseChatModel.""" + try: + assert issubclass(OpenRouterAgent, BaseAgentModel), "Must inherit from BaseAgentModel" + assert issubclass(OpenRouterAgent, BaseChatModel), "Must inherit from BaseChatModel" + return True, "OpenRouterAgent inheritance check passed" + except Exception as E: + return False, str(E) + +def case_agent_methods(): + """Verify OpenRouterAgent implements generate, get_params, and __call__.""" + try: + assert callable(getattr(OpenRouterAgent, "generate", None)), "generate must be callable" + assert callable(getattr(OpenRouterAgent, "get_params", None)), "get_params must be callable" + assert callable(getattr(OpenRouterAgent, "__call__", None)), "__call__ must be callable" + return True, "OpenRouterAgent methods check passed" + except Exception as E: + return False, str(E) + diff --git a/tests/openrouter_client_test.py b/tests/openrouter_client_test.py new file mode 100644 index 0000000..f486e4a --- /dev/null +++ b/tests/openrouter_client_test.py @@ -0,0 +1,30 @@ +# RSRoute +# Copyright (c) 2026 ItzRustam +# SPDX-License-Identifier: BSD-3-Clause + +from app.providers import ChatOpenRouter +from app.Errors import InvalidRequest + +"NOTE: Test Function only on Fake API_KEY, Already tested on Real OpenRouter API key." +def test_openrouter(KEY : str = None, query : str = None, real_api = False): + # Model Initialize + try: + model = ChatOpenRouter(api_key=KEY) + except InvalidRequest as ir: + return True, ir + except Exception as e: + return False, e + + try: + output = model(query=query) + except InvalidRequest as ir: + return True, ir + except Exception as e: + if real_api: + return False, e + return True, e + else: + if not real_api: + return False, "Unexpected Error Occur." + return True, output +