From 556711d910d72449a2872b3624b13c3cf1a1d9be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 12:24:59 +0000 Subject: [PATCH 1/2] Add API-compatible Ollama support with environment configuration - Added OLLAMA_BASE_URL, OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, and OLLAMA_TEMPERATURE to .env.template - Updated agent.py to use Ollama environment variables for ChatOllama initialization - Updated retriever.py to use Ollama environment variables for OllamaEmbeddings - Updated tools/summarize_text.py to use Ollama environment variables - Created .env file as a working example (not tracked in git) - Now supports both local Ollama (http://localhost:11434) and remote/API Ollama instances Users can now configure Ollama to point to any API-compatible endpoint by setting OLLAMA_BASE_URL in their .env file. --- .env.template | 12 ++++++++++++ agent.py | 11 ++++++++++- retriever.py | 13 ++++++++++++- tools/summarize_text.py | 17 +++++++++++++++-- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.env.template b/.env.template index 70a0372..7cc3eec 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,18 @@ SYSTEM_PROMPT_PATH=./prompts/research_agent_prompt.txt + +# Ollama Configuration +# For local Ollama: http://localhost:11434 +# For remote/API Ollama: http://your-ollama-server:11434 +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_CHAT_MODEL=gpt-oss:120b-cloud +OLLAMA_EMBED_MODEL=nomic-embed-text +OLLAMA_TEMPERATURE=0.7 + +# API Keys ASKNEWS_CLIENT_ID = ASKNEWS_CLIENT_SECRET = SERPAPI_API_KEY = + +# SearxNG Configuration SEARX_INSTANCE_URL = SEARX_TOP_K_RESULTS = 5 # Number of top search results to retrieve \ No newline at end of file diff --git a/agent.py b/agent.py index c8cbb2e..fac6724 100644 --- a/agent.py +++ b/agent.py @@ -29,7 +29,16 @@ def main(): x = input("Ask Kurama 🦊\n") load_dotenv() - llm = ChatOllama(model="gpt-oss:120b-cloud", temperature=0.7) + # Configure Ollama from environment variables + ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + ollama_model = os.getenv("OLLAMA_CHAT_MODEL", "gpt-oss:120b-cloud") + ollama_temperature = float(os.getenv("OLLAMA_TEMPERATURE", "0.7")) + + llm = ChatOllama( + model=ollama_model, + temperature=ollama_temperature, + base_url=ollama_base_url + ) recursion_limit = 100 config = RunnableConfig(tags=["debug", "local"], recursion_limit=recursion_limit) agent = create_agent( diff --git a/retriever.py b/retriever.py index 5c5a739..3a07dbe 100644 --- a/retriever.py +++ b/retriever.py @@ -3,8 +3,19 @@ from langchain.tools import tool import uuid from utils.spinner import Spinner +import os +from dotenv import load_dotenv -embed = OllamaEmbeddings(model="nomic-embed-text") +load_dotenv() + +# Configure Ollama embeddings from environment variables +ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") +ollama_embed_model = os.getenv("OLLAMA_EMBED_MODEL", "nomic-embed-text") + +embed = OllamaEmbeddings( + model=ollama_embed_model, + base_url=ollama_base_url +) @tool def add_to_db(text: str, metadata: dict = None): diff --git a/tools/summarize_text.py b/tools/summarize_text.py index 7172061..8f89010 100644 --- a/tools/summarize_text.py +++ b/tools/summarize_text.py @@ -1,17 +1,30 @@ from langchain_ollama import ChatOllama from langchain.tools import tool from utils.spinner import Spinner +import os +from dotenv import load_dotenv + +load_dotenv() @tool def summarize_text(text: str) -> str: """ - Summarizes a given text using the local Ollama LLM. + Summarizes a given text using Ollama LLM (local or remote API). Useful for summarizing research reports, markdowns, or long notes. """ s = Spinner("Running summarize_text…") s.start() try: - llm = ChatOllama(model="gpt-oss:120b-cloud", temperature=0.7) + # Configure Ollama from environment variables + ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + ollama_model = os.getenv("OLLAMA_CHAT_MODEL", "gpt-oss:120b-cloud") + ollama_temperature = float(os.getenv("OLLAMA_TEMPERATURE", "0.7")) + + llm = ChatOllama( + model=ollama_model, + temperature=ollama_temperature, + base_url=ollama_base_url + ) prompt = ( "Summarize the following text in clear, structured Markdown format. " "Keep all essential details, and include a 'Key Takeaway' section at the end:\n\n" From 99cfdbbfe3c936a5084dd007bfa28d3eb6369802 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 12:32:29 +0000 Subject: [PATCH 2/2] Add multi-provider LLM support with API key authentication This commit adds comprehensive support for multiple LLM providers, allowing users to easily switch between different services: New Features: - Multi-provider architecture supporting Ollama, LM Studio, OpenAI, and OpenAI-compatible APIs - API key authentication support for all providers - Unified configuration through .env file - Easy provider switching via LLM_PROVIDER and EMBEDDING_PROVIDER variables Supported Providers: - Ollama (local/remote with optional API key) - LM Studio (local OpenAI-compatible server) - OpenAI (official API) - OpenAI-Compatible (OpenRouter, Together.ai, Groq, Fireworks, etc.) Changes: - Created utils/llm_config.py - Centralized LLM provider management - Updated .env.template - Added configuration for all providers - Updated agent.py - Now uses provider configuration system - Updated retriever.py - Now uses provider configuration for embeddings - Updated tools/summarize_text.py - Now uses provider configuration - Updated requirements.txt - Added langchain-openai dependency - Added PROVIDER_EXAMPLES.md - Comprehensive configuration examples and troubleshooting Users can now: 1. Use local models (Ollama, LM Studio) 2. Use cloud APIs (OpenAI, OpenRouter, Groq, Together.ai, Fireworks) 3. Mix providers (e.g., Groq for chat, OpenAI for embeddings) 4. Switch providers by changing a single environment variable --- .env.template | 43 ++++++- PROVIDER_EXAMPLES.md | 259 ++++++++++++++++++++++++++++++++++++++++ agent.py | 14 +-- requirements.txt | 3 +- retriever.py | 13 +- tools/summarize_text.py | 17 +-- utils/llm_config.py | 229 +++++++++++++++++++++++++++++++++++ 7 files changed, 539 insertions(+), 39 deletions(-) create mode 100644 PROVIDER_EXAMPLES.md create mode 100644 utils/llm_config.py diff --git a/.env.template b/.env.template index 7cc3eec..215b479 100644 --- a/.env.template +++ b/.env.template @@ -1,14 +1,49 @@ SYSTEM_PROMPT_PATH=./prompts/research_agent_prompt.txt +# ============================================================================= +# LLM Provider Configuration +# ============================================================================= +# Choose your provider: ollama, lmstudio, openai, openai_compatible +LLM_PROVIDER=ollama +EMBEDDING_PROVIDER=ollama + +# Model Configuration +CHAT_MODEL=gpt-oss:120b-cloud +EMBED_MODEL=nomic-embed-text +TEMPERATURE=0.7 + +# ----------------------------------------------------------------------------- # Ollama Configuration +# ----------------------------------------------------------------------------- # For local Ollama: http://localhost:11434 # For remote/API Ollama: http://your-ollama-server:11434 OLLAMA_BASE_URL=http://localhost:11434 -OLLAMA_CHAT_MODEL=gpt-oss:120b-cloud -OLLAMA_EMBED_MODEL=nomic-embed-text -OLLAMA_TEMPERATURE=0.7 +OLLAMA_API_KEY= # Optional, leave empty if not needed + +# ----------------------------------------------------------------------------- +# LM Studio Configuration +# ----------------------------------------------------------------------------- +# Default LM Studio local server +LMSTUDIO_BASE_URL=http://localhost:1234/v1 +LMSTUDIO_API_KEY= # Optional, leave empty if not needed + +# ----------------------------------------------------------------------------- +# OpenAI Configuration +# ----------------------------------------------------------------------------- +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api.openai.com/v1 # Optional, can override +OPENAI_ORGANIZATION= # Optional + +# ----------------------------------------------------------------------------- +# OpenAI-Compatible Configuration (OpenRouter, Together.ai, Groq, etc.) +# ----------------------------------------------------------------------------- +OPENAI_COMPATIBLE_BASE_URL=https://api.openrouter.ai/v1 +OPENAI_COMPATIBLE_API_KEY= +OPENAI_COMPATIBLE_MODEL= # Optional, override CHAT_MODEL if needed -# API Keys +# ============================================================================= +# External API Keys +# ============================================================================= ASKNEWS_CLIENT_ID = ASKNEWS_CLIENT_SECRET = SERPAPI_API_KEY = diff --git a/PROVIDER_EXAMPLES.md b/PROVIDER_EXAMPLES.md new file mode 100644 index 0000000..9ec2bdb --- /dev/null +++ b/PROVIDER_EXAMPLES.md @@ -0,0 +1,259 @@ +# LLM Provider Configuration Examples + +This guide shows you how to configure different LLM providers with PersonalAgent. + +## Supported Providers + +1. **Ollama** (local/remote) +2. **LM Studio** (local) +3. **OpenAI** (API) +4. **OpenAI-Compatible** (OpenRouter, Together.ai, Groq, Fireworks, etc.) + +## Quick Start + +1. Copy `.env.template` to `.env` +2. Set your `LLM_PROVIDER` and configure the relevant section +3. Run the agent! + +--- + +## Configuration Examples + +### 1. Ollama (Local) + +Default setup for local Ollama installation: + +```bash +LLM_PROVIDER=ollama +EMBEDDING_PROVIDER=ollama + +CHAT_MODEL=gpt-oss:120b-cloud +EMBED_MODEL=nomic-embed-text +TEMPERATURE=0.7 + +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_API_KEY= # Leave empty for local +``` + +### 2. Ollama (Remote/API) + +Connect to a remote Ollama instance: + +```bash +LLM_PROVIDER=ollama +EMBEDDING_PROVIDER=ollama + +CHAT_MODEL=llama3.1:70b +EMBED_MODEL=nomic-embed-text +TEMPERATURE=0.7 + +OLLAMA_BASE_URL=http://your-server.com:11434 +OLLAMA_API_KEY=your_api_key_if_needed +``` + +### 3. LM Studio (Local) + +Use LM Studio's local server: + +```bash +LLM_PROVIDER=lmstudio +EMBEDDING_PROVIDER=lmstudio + +CHAT_MODEL=llama-3.1-8b # Or whatever model you have loaded +EMBED_MODEL=nomic-embed-text +TEMPERATURE=0.7 + +LMSTUDIO_BASE_URL=http://localhost:1234/v1 +LMSTUDIO_API_KEY= # LM Studio uses a dummy key +``` + +**Note:** Make sure LM Studio's local server is running before starting the agent. + +### 4. OpenAI + +Use OpenAI's official API: + +```bash +LLM_PROVIDER=openai +EMBEDDING_PROVIDER=openai + +CHAT_MODEL=gpt-4o +EMBED_MODEL=text-embedding-3-small +TEMPERATURE=0.7 + +OPENAI_API_KEY=sk-your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 # Optional +OPENAI_ORGANIZATION= # Optional +``` + +### 5. OpenRouter + +Use OpenRouter to access multiple models: + +```bash +LLM_PROVIDER=openai_compatible +EMBEDDING_PROVIDER=openai_compatible + +CHAT_MODEL=anthropic/claude-3.5-sonnet +EMBED_MODEL=text-embedding-3-small +TEMPERATURE=0.7 + +OPENAI_COMPATIBLE_BASE_URL=https://openrouter.ai/api/v1 +OPENAI_COMPATIBLE_API_KEY=sk-or-v1-your-api-key +``` + +**Popular OpenRouter models:** +- `anthropic/claude-3.5-sonnet` +- `meta-llama/llama-3.1-405b-instruct` +- `google/gemini-pro-1.5` +- `openai/gpt-4o` + +### 6. Together.ai + +Use Together.ai for fast inference: + +```bash +LLM_PROVIDER=openai_compatible +EMBEDDING_PROVIDER=openai_compatible + +CHAT_MODEL=meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo +EMBED_MODEL=togethercomputer/m2-bert-80M-8k-retrieval +TEMPERATURE=0.7 + +OPENAI_COMPATIBLE_BASE_URL=https://api.together.xyz/v1 +OPENAI_COMPATIBLE_API_KEY=your_together_api_key +``` + +### 7. Groq + +Use Groq for ultra-fast inference: + +```bash +LLM_PROVIDER=openai_compatible +EMBEDDING_PROVIDER=openai # Groq doesn't have embeddings, use OpenAI + +CHAT_MODEL=llama-3.1-70b-versatile +EMBED_MODEL=text-embedding-3-small +TEMPERATURE=0.7 + +OPENAI_COMPATIBLE_BASE_URL=https://api.groq.com/openai/v1 +OPENAI_COMPATIBLE_API_KEY=gsk_your_groq_api_key + +# For embeddings +OPENAI_API_KEY=sk-your-openai-api-key +``` + +**Popular Groq models:** +- `llama-3.1-70b-versatile` +- `llama-3.1-8b-instant` +- `mixtral-8x7b-32768` +- `gemma2-9b-it` + +### 8. Fireworks.ai + +Use Fireworks.ai for fast model hosting: + +```bash +LLM_PROVIDER=openai_compatible +EMBEDDING_PROVIDER=openai_compatible + +CHAT_MODEL=accounts/fireworks/models/llama-v3p1-70b-instruct +EMBED_MODEL=nomic-ai/nomic-embed-text-v1.5 +TEMPERATURE=0.7 + +OPENAI_COMPATIBLE_BASE_URL=https://api.fireworks.ai/inference/v1 +OPENAI_COMPATIBLE_API_KEY=fw_your_fireworks_api_key +``` + +### 9. Mixed Providers + +You can use different providers for chat and embeddings: + +```bash +# Use Groq for fast chat, OpenAI for embeddings +LLM_PROVIDER=openai_compatible +EMBEDDING_PROVIDER=openai + +CHAT_MODEL=llama-3.1-70b-versatile +EMBED_MODEL=text-embedding-3-small +TEMPERATURE=0.7 + +# Groq for chat +OPENAI_COMPATIBLE_BASE_URL=https://api.groq.com/openai/v1 +OPENAI_COMPATIBLE_API_KEY=gsk_your_groq_api_key + +# OpenAI for embeddings +OPENAI_API_KEY=sk-your-openai-api-key +``` + +--- + +## Model Recommendations + +### Chat Models + +**For Quality:** +- `gpt-4o` (OpenAI) +- `claude-3.5-sonnet` (OpenRouter) +- `llama-3.1-405b-instruct` (OpenRouter/Together.ai) + +**For Speed:** +- `llama-3.1-8b-instant` (Groq) +- `llama-3.1-70b-versatile` (Groq) +- `mixtral-8x7b-32768` (Groq) + +**For Local:** +- `llama3.1:70b` (Ollama) +- `mistral:7b` (Ollama) +- `qwen2.5:32b` (Ollama) + +### Embedding Models + +**OpenAI:** +- `text-embedding-3-small` (fast, cheap) +- `text-embedding-3-large` (best quality) + +**Local (Ollama):** +- `nomic-embed-text` (recommended) +- `mxbai-embed-large` +- `all-minilm` + +--- + +## Troubleshooting + +### "Connection refused" errors +- Check that your local server (Ollama/LM Studio) is running +- Verify the BASE_URL is correct +- Check firewall settings + +### "Invalid API key" errors +- Double-check your API key is correct +- Make sure there are no extra spaces +- Verify the API key has proper permissions + +### "Model not found" errors +- Ensure the model name matches exactly (case-sensitive) +- For Ollama: run `ollama list` to see available models +- For LM Studio: check the model is loaded in the UI + +### Import errors +- Run `pip install -r requirements.txt` to install all dependencies +- Make sure `langchain-openai` is installed for OpenAI-compatible providers + +--- + +## Cost Considerations + +**Free/Local:** +- Ollama (local) +- LM Studio (local) + +**Pay-as-you-go:** +- OpenAI: $$$ (most expensive, best quality) +- OpenRouter: $$ (varies by model) +- Together.ai: $ (affordable, fast) +- Groq: $ (very fast, affordable) +- Fireworks.ai: $ (affordable) + +**Tip:** Start with Groq's free tier or use local Ollama for development! diff --git a/agent.py b/agent.py index fac6724..039003a 100644 --- a/agent.py +++ b/agent.py @@ -1,4 +1,3 @@ -from langchain_ollama import ChatOllama from langchain.agents import create_agent from langchain_core.runnables import RunnableConfig from tools.wiki import wiki_search @@ -22,6 +21,7 @@ import os from utils.spinner import Spinner from utils.markdown_render import render_markdown +from utils.llm_config import get_chat_model from langgraph.errors import GraphRecursionError @@ -29,16 +29,8 @@ def main(): x = input("Ask Kurama 🦊\n") load_dotenv() - # Configure Ollama from environment variables - ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") - ollama_model = os.getenv("OLLAMA_CHAT_MODEL", "gpt-oss:120b-cloud") - ollama_temperature = float(os.getenv("OLLAMA_TEMPERATURE", "0.7")) - - llm = ChatOllama( - model=ollama_model, - temperature=ollama_temperature, - base_url=ollama_base_url - ) + # Get chat model based on configured provider + llm = get_chat_model() recursion_limit = 100 config = RunnableConfig(tags=["debug", "local"], recursion_limit=recursion_limit) agent = create_agent( diff --git a/requirements.txt b/requirements.txt index eb6dea0..8cdf52d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ langchain>=0.3.0 langchain-core>=0.3.0 langchain-community>=0.3.0 langchain-ollama>=0.1.0 +langchain-openai>=0.2.0 langgraph>=0.1.0 duckduckgo-search>=6.3.0 @@ -9,7 +10,7 @@ ddgs>=1.1.6 wikipedia>=1.4.0 requests>=2.31.0 -chromadb>=0.5.5 +chromadb>=0.5.5 python-dotenv>=1.0.1 pydantic>=2.8.2 tqdm>=4.66.1 diff --git a/retriever.py b/retriever.py index 3a07dbe..c33c736 100644 --- a/retriever.py +++ b/retriever.py @@ -1,21 +1,14 @@ -from langchain_ollama import OllamaEmbeddings from store.chromadb import collection from langchain.tools import tool import uuid from utils.spinner import Spinner -import os +from utils.llm_config import get_embedding_model from dotenv import load_dotenv load_dotenv() -# Configure Ollama embeddings from environment variables -ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") -ollama_embed_model = os.getenv("OLLAMA_EMBED_MODEL", "nomic-embed-text") - -embed = OllamaEmbeddings( - model=ollama_embed_model, - base_url=ollama_base_url -) +# Get embedding model based on configured provider +embed = get_embedding_model() @tool def add_to_db(text: str, metadata: dict = None): diff --git a/tools/summarize_text.py b/tools/summarize_text.py index 8f89010..72b3dbe 100644 --- a/tools/summarize_text.py +++ b/tools/summarize_text.py @@ -1,7 +1,6 @@ -from langchain_ollama import ChatOllama from langchain.tools import tool from utils.spinner import Spinner -import os +from utils.llm_config import get_chat_model from dotenv import load_dotenv load_dotenv() @@ -9,22 +8,14 @@ @tool def summarize_text(text: str) -> str: """ - Summarizes a given text using Ollama LLM (local or remote API). + Summarizes a given text using the configured LLM provider. Useful for summarizing research reports, markdowns, or long notes. """ s = Spinner("Running summarize_text…") s.start() try: - # Configure Ollama from environment variables - ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") - ollama_model = os.getenv("OLLAMA_CHAT_MODEL", "gpt-oss:120b-cloud") - ollama_temperature = float(os.getenv("OLLAMA_TEMPERATURE", "0.7")) - - llm = ChatOllama( - model=ollama_model, - temperature=ollama_temperature, - base_url=ollama_base_url - ) + # Get chat model based on configured provider + llm = get_chat_model() prompt = ( "Summarize the following text in clear, structured Markdown format. " "Keep all essential details, and include a 'Key Takeaway' section at the end:\n\n" diff --git a/utils/llm_config.py b/utils/llm_config.py new file mode 100644 index 0000000..359bbb0 --- /dev/null +++ b/utils/llm_config.py @@ -0,0 +1,229 @@ +""" +LLM Provider Configuration Manager + +Supports multiple LLM providers: +- Ollama (local/remote) +- LM Studio +- OpenAI +- OpenAI-compatible (OpenRouter, Together.ai, Groq, etc.) +""" + +import os +from typing import Optional +from dotenv import load_dotenv + +load_dotenv() + + +class LLMConfig: + """Configuration manager for LLM providers""" + + def __init__(self): + self.provider = os.getenv("LLM_PROVIDER", "ollama").lower() + self.embedding_provider = os.getenv("EMBEDDING_PROVIDER", "ollama").lower() + self.chat_model = os.getenv("CHAT_MODEL", "gpt-oss:120b-cloud") + self.embed_model = os.getenv("EMBED_MODEL", "nomic-embed-text") + self.temperature = float(os.getenv("TEMPERATURE", "0.7")) + + def get_chat_model(self): + """Get configured chat model based on provider""" + from langchain_ollama import ChatOllama + from langchain_openai import ChatOpenAI + + if self.provider == "ollama": + return self._get_ollama_chat() + elif self.provider == "lmstudio": + return self._get_lmstudio_chat() + elif self.provider == "openai": + return self._get_openai_chat() + elif self.provider == "openai_compatible": + return self._get_openai_compatible_chat() + else: + raise ValueError(f"Unsupported LLM provider: {self.provider}") + + def get_embedding_model(self): + """Get configured embedding model based on provider""" + from langchain_ollama import OllamaEmbeddings + from langchain_openai import OpenAIEmbeddings + + if self.embedding_provider == "ollama": + return self._get_ollama_embeddings() + elif self.embedding_provider == "lmstudio": + return self._get_lmstudio_embeddings() + elif self.embedding_provider == "openai": + return self._get_openai_embeddings() + elif self.embedding_provider == "openai_compatible": + return self._get_openai_compatible_embeddings() + else: + raise ValueError(f"Unsupported embedding provider: {self.embedding_provider}") + + def _get_ollama_chat(self): + """Configure Ollama chat model""" + from langchain_ollama import ChatOllama + + base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + api_key = os.getenv("OLLAMA_API_KEY") + + kwargs = { + "model": self.chat_model, + "temperature": self.temperature, + "base_url": base_url, + } + + if api_key: + kwargs["api_key"] = api_key + + return ChatOllama(**kwargs) + + def _get_ollama_embeddings(self): + """Configure Ollama embeddings""" + from langchain_ollama import OllamaEmbeddings + + base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + api_key = os.getenv("OLLAMA_API_KEY") + + kwargs = { + "model": self.embed_model, + "base_url": base_url, + } + + if api_key: + kwargs["api_key"] = api_key + + return OllamaEmbeddings(**kwargs) + + def _get_lmstudio_chat(self): + """Configure LM Studio chat model (OpenAI-compatible)""" + from langchain_openai import ChatOpenAI + + base_url = os.getenv("LMSTUDIO_BASE_URL", "http://localhost:1234/v1") + api_key = os.getenv("LMSTUDIO_API_KEY", "lm-studio") # LM Studio uses dummy key + + return ChatOpenAI( + model=self.chat_model, + temperature=self.temperature, + base_url=base_url, + api_key=api_key, + ) + + def _get_lmstudio_embeddings(self): + """Configure LM Studio embeddings (OpenAI-compatible)""" + from langchain_openai import OpenAIEmbeddings + + base_url = os.getenv("LMSTUDIO_BASE_URL", "http://localhost:1234/v1") + api_key = os.getenv("LMSTUDIO_API_KEY", "lm-studio") + + return OpenAIEmbeddings( + model=self.embed_model, + base_url=base_url, + api_key=api_key, + ) + + def _get_openai_chat(self): + """Configure OpenAI chat model""" + from langchain_openai import ChatOpenAI + + api_key = os.getenv("OPENAI_API_KEY") + if not api_key or api_key.startswith("<"): + raise ValueError("OPENAI_API_KEY is required for OpenAI provider") + + kwargs = { + "model": self.chat_model, + "temperature": self.temperature, + "api_key": api_key, + } + + # Optional overrides + base_url = os.getenv("OPENAI_BASE_URL") + if base_url: + kwargs["base_url"] = base_url + + organization = os.getenv("OPENAI_ORGANIZATION") + if organization: + kwargs["organization"] = organization + + return ChatOpenAI(**kwargs) + + def _get_openai_embeddings(self): + """Configure OpenAI embeddings""" + from langchain_openai import OpenAIEmbeddings + + api_key = os.getenv("OPENAI_API_KEY") + if not api_key or api_key.startswith("<"): + raise ValueError("OPENAI_API_KEY is required for OpenAI provider") + + kwargs = { + "model": self.embed_model, + "api_key": api_key, + } + + base_url = os.getenv("OPENAI_BASE_URL") + if base_url: + kwargs["base_url"] = base_url + + organization = os.getenv("OPENAI_ORGANIZATION") + if organization: + kwargs["organization"] = organization + + return OpenAIEmbeddings(**kwargs) + + def _get_openai_compatible_chat(self): + """Configure OpenAI-compatible chat model (OpenRouter, Together.ai, Groq, etc.)""" + from langchain_openai import ChatOpenAI + + base_url = os.getenv("OPENAI_COMPATIBLE_BASE_URL") + api_key = os.getenv("OPENAI_COMPATIBLE_API_KEY") + + if not base_url: + raise ValueError("OPENAI_COMPATIBLE_BASE_URL is required for openai_compatible provider") + if not api_key or api_key.startswith("<"): + raise ValueError("OPENAI_COMPATIBLE_API_KEY is required for openai_compatible provider") + + # Allow override of model for provider-specific models + model = os.getenv("OPENAI_COMPATIBLE_MODEL") or self.chat_model + + return ChatOpenAI( + model=model, + temperature=self.temperature, + base_url=base_url, + api_key=api_key, + ) + + def _get_openai_compatible_embeddings(self): + """Configure OpenAI-compatible embeddings""" + from langchain_openai import OpenAIEmbeddings + + base_url = os.getenv("OPENAI_COMPATIBLE_BASE_URL") + api_key = os.getenv("OPENAI_COMPATIBLE_API_KEY") + + if not base_url: + raise ValueError("OPENAI_COMPATIBLE_BASE_URL is required for openai_compatible provider") + if not api_key or api_key.startswith("<"): + raise ValueError("OPENAI_COMPATIBLE_API_KEY is required for openai_compatible provider") + + return OpenAIEmbeddings( + model=self.embed_model, + base_url=base_url, + api_key=api_key, + ) + + +# Singleton instance +_config = None + +def get_llm_config() -> LLMConfig: + """Get or create LLM configuration singleton""" + global _config + if _config is None: + _config = LLMConfig() + return _config + + +def get_chat_model(): + """Convenience function to get chat model""" + return get_llm_config().get_chat_model() + + +def get_embedding_model(): + """Convenience function to get embedding model""" + return get_llm_config().get_embedding_model()