rohit
commited on
Commit
Β·
0a1d4cf
1
Parent(s):
f2815c1
Integrate OpenRouter GLM model with RAG tool calling
Browse files- Replace Google Gemini with OpenRouter GLM-4.5-air model
- Add intelligent tool calling for RAG pipeline access
- Implement new /chat endpoint with multi-turn conversation support
- Remove Google API key dependency
- Add comprehensive .gitignore for Python projects
- Maintain backward compatibility with existing /answer endpoint
Features:
- GLM model automatically decides when to use RAG vs general chat
- Tool calling architecture for extensible functionality
- Background dataset loading for faster startup
- Support for developer-portfolio and other datasets
- .gitignore +163 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/pipeline.cpython-311.pyc +0 -0
- app/config.py +2 -3
- app/main.py +190 -19
- app/pipeline.py +29 -27
- test_openrouter_connection.py +264 -0
.gitignore
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
pip-wheel-metadata/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
MANIFEST
|
| 29 |
+
|
| 30 |
+
# PyInstaller
|
| 31 |
+
# Usually these files are written by a python script from a template
|
| 32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 33 |
+
*.manifest
|
| 34 |
+
*.spec
|
| 35 |
+
|
| 36 |
+
# Installer logs
|
| 37 |
+
pip-log.txt
|
| 38 |
+
pip-delete-this-directory.txt
|
| 39 |
+
|
| 40 |
+
# Unit test / coverage reports
|
| 41 |
+
htmlcov/
|
| 42 |
+
.tox/
|
| 43 |
+
.nox/
|
| 44 |
+
.coverage
|
| 45 |
+
.coverage.*
|
| 46 |
+
.cache
|
| 47 |
+
nosetests.xml
|
| 48 |
+
coverage.xml
|
| 49 |
+
*.cover
|
| 50 |
+
*.py,cover
|
| 51 |
+
.hypothesis/
|
| 52 |
+
.pytest_cache/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
target/
|
| 76 |
+
|
| 77 |
+
# Jupyter Notebook
|
| 78 |
+
.ipynb_checkpoints
|
| 79 |
+
|
| 80 |
+
# IPython
|
| 81 |
+
profile_default/
|
| 82 |
+
ipython_config.py
|
| 83 |
+
|
| 84 |
+
# pyenv
|
| 85 |
+
.python-version
|
| 86 |
+
|
| 87 |
+
# pipenv
|
| 88 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 89 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 90 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 91 |
+
# install all needed dependencies.
|
| 92 |
+
#Pipfile.lock
|
| 93 |
+
|
| 94 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
| 95 |
+
__pypackages__/
|
| 96 |
+
|
| 97 |
+
# Celery stuff
|
| 98 |
+
celerybeat-schedule
|
| 99 |
+
celerybeat.pid
|
| 100 |
+
|
| 101 |
+
# SageMath parsed files
|
| 102 |
+
*.sage.py
|
| 103 |
+
|
| 104 |
+
# Environments
|
| 105 |
+
.env
|
| 106 |
+
.venv
|
| 107 |
+
env/
|
| 108 |
+
venv/
|
| 109 |
+
ENV/
|
| 110 |
+
env.bak/
|
| 111 |
+
venv.bak/
|
| 112 |
+
|
| 113 |
+
# Spyder project settings
|
| 114 |
+
.spyderproject
|
| 115 |
+
.spyproject
|
| 116 |
+
|
| 117 |
+
# Rope project settings
|
| 118 |
+
.ropeproject
|
| 119 |
+
|
| 120 |
+
# mkdocs documentation
|
| 121 |
+
/site
|
| 122 |
+
|
| 123 |
+
# mypy
|
| 124 |
+
.mypy_cache/
|
| 125 |
+
.dmypy.json
|
| 126 |
+
dmypy.json
|
| 127 |
+
|
| 128 |
+
# Pyre type checker
|
| 129 |
+
.pyre/
|
| 130 |
+
|
| 131 |
+
# IDE
|
| 132 |
+
.vscode/
|
| 133 |
+
.idea/
|
| 134 |
+
*.swp
|
| 135 |
+
*.swo
|
| 136 |
+
*~
|
| 137 |
+
|
| 138 |
+
# OS
|
| 139 |
+
.DS_Store
|
| 140 |
+
Thumbs.db
|
| 141 |
+
|
| 142 |
+
# Model files
|
| 143 |
+
*.pkl
|
| 144 |
+
*.joblib
|
| 145 |
+
*.bin
|
| 146 |
+
*.onnx
|
| 147 |
+
|
| 148 |
+
# Data files
|
| 149 |
+
data/
|
| 150 |
+
datasets/
|
| 151 |
+
*.csv
|
| 152 |
+
*.json
|
| 153 |
+
*.jsonl
|
| 154 |
+
*.parquet
|
| 155 |
+
|
| 156 |
+
# Logs
|
| 157 |
+
logs/
|
| 158 |
+
*.log
|
| 159 |
+
|
| 160 |
+
# Temporary files
|
| 161 |
+
tmp/
|
| 162 |
+
temp/
|
| 163 |
+
*.tmp
|
app/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/__init__.cpython-311.pyc and b/app/__pycache__/__init__.cpython-311.pyc differ
|
|
|
app/__pycache__/config.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
|
|
|
app/__pycache__/main.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/main.cpython-311.pyc and b/app/__pycache__/main.cpython-311.pyc differ
|
|
|
app/__pycache__/pipeline.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/pipeline.cpython-311.pyc and b/app/__pycache__/pipeline.cpython-311.pyc differ
|
|
|
app/config.py
CHANGED
|
@@ -7,7 +7,7 @@ class DatasetConfig:
|
|
| 7 |
name: str
|
| 8 |
split: str = "train"
|
| 9 |
content_field: str = "content"
|
| 10 |
-
fields: Dict[str, str] = None # Dictionary of field mappings
|
| 11 |
prompt_template: Optional[str] = None
|
| 12 |
|
| 13 |
# Default configurations for different datasets
|
|
@@ -164,8 +164,7 @@ DATASET_CONFIGS = {
|
|
| 164 |
),
|
| 165 |
}
|
| 166 |
|
| 167 |
-
# Default configuration for embedding
|
| 168 |
MODEL_CONFIG = {
|
| 169 |
"embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
|
| 170 |
-
"llm_model": "gemini-2.0-flash-exp",
|
| 171 |
}
|
|
|
|
| 7 |
name: str
|
| 8 |
split: str = "train"
|
| 9 |
content_field: str = "content"
|
| 10 |
+
fields: Optional[Dict[str, str]] = None # Dictionary of field mappings
|
| 11 |
prompt_template: Optional[str] = None
|
| 12 |
|
| 13 |
# Default configurations for different datasets
|
|
|
|
| 164 |
),
|
| 165 |
}
|
| 166 |
|
| 167 |
+
# Default configuration for embedding model
|
| 168 |
MODEL_CONFIG = {
|
| 169 |
"embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
|
|
|
|
| 170 |
}
|
app/main.py
CHANGED
|
@@ -4,6 +4,10 @@ import os
|
|
| 4 |
import logging
|
| 5 |
import sys
|
| 6 |
from .config import DATASET_CONFIGS
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# Lazy imports to avoid blocking startup
|
| 8 |
# from .pipeline import RAGPipeline # Will import when needed
|
| 9 |
# import umap # Will import when needed for visualization
|
|
@@ -13,7 +17,6 @@ from .config import DATASET_CONFIGS
|
|
| 13 |
# import numpy as np # Will import when needed for visualization
|
| 14 |
# from sklearn.preprocessing import normalize # Will import when needed for visualization
|
| 15 |
# import pandas as pd # Will import when needed for visualization
|
| 16 |
-
import json
|
| 17 |
|
| 18 |
# Configure logging
|
| 19 |
logging.basicConfig(
|
|
@@ -27,6 +30,15 @@ logger = logging.getLogger(__name__)
|
|
| 27 |
|
| 28 |
app = FastAPI(title="RAG Pipeline API", description="Multi-dataset RAG API", version="1.0.0")
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Initialize pipelines for all datasets
|
| 31 |
pipelines = {}
|
| 32 |
google_api_key = os.getenv("GOOGLE_API_KEY")
|
|
@@ -36,6 +48,59 @@ logger.info(f"Port from env: {os.getenv('PORT', 'Not set - will use 8000')}")
|
|
| 36 |
logger.info(f"Google API Key present: {'Yes' if google_api_key else 'No'}")
|
| 37 |
logger.info(f"Available datasets: {list(DATASET_CONFIGS.keys())}")
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Don't load datasets during startup - do it asynchronously after server starts
|
| 40 |
logger.info("RAG Pipeline API is ready to serve requests - datasets will load in background")
|
| 41 |
|
|
@@ -47,6 +112,118 @@ class Question(BaseModel):
|
|
| 47 |
text: str
|
| 48 |
dataset: str = "developer-portfolio" # Default dataset
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
@app.post("/answer")
|
| 51 |
async def get_answer(question: Question):
|
| 52 |
try:
|
|
@@ -86,24 +263,18 @@ async def list_questions(dataset: str = "developer-portfolio"):
|
|
| 86 |
async def load_datasets_background():
|
| 87 |
"""Load datasets in background after server starts"""
|
| 88 |
global pipelines
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
logger.info(f"Successfully loaded {dataset_name}")
|
| 102 |
-
except Exception as e:
|
| 103 |
-
logger.error(f"Failed to load {dataset_name}: {e}")
|
| 104 |
-
logger.info(f"Background loading complete - {len(pipelines)} datasets loaded")
|
| 105 |
-
else:
|
| 106 |
-
logger.warning("No Google API key provided - running in demo mode without datasets")
|
| 107 |
|
| 108 |
@app.on_event("startup")
|
| 109 |
async def startup_event():
|
|
|
|
| 4 |
import logging
|
| 5 |
import sys
|
| 6 |
from .config import DATASET_CONFIGS
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
from openai.types.chat import ChatCompletionMessageParam
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
# Lazy imports to avoid blocking startup
|
| 12 |
# from .pipeline import RAGPipeline # Will import when needed
|
| 13 |
# import umap # Will import when needed for visualization
|
|
|
|
| 17 |
# import numpy as np # Will import when needed for visualization
|
| 18 |
# from sklearn.preprocessing import normalize # Will import when needed for visualization
|
| 19 |
# import pandas as pd # Will import when needed for visualization
|
|
|
|
| 20 |
|
| 21 |
# Configure logging
|
| 22 |
logging.basicConfig(
|
|
|
|
| 30 |
|
| 31 |
app = FastAPI(title="RAG Pipeline API", description="Multi-dataset RAG API", version="1.0.0")
|
| 32 |
|
| 33 |
+
# Initialize OpenRouter client
|
| 34 |
+
openrouter_client = OpenAI(
|
| 35 |
+
base_url="https://openrouter.ai/api/v1",
|
| 36 |
+
api_key="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"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Model configuration
|
| 40 |
+
MODEL_NAME = "z-ai/glm-4.5-air:free"
|
| 41 |
+
|
| 42 |
# Initialize pipelines for all datasets
|
| 43 |
pipelines = {}
|
| 44 |
google_api_key = os.getenv("GOOGLE_API_KEY")
|
|
|
|
| 48 |
logger.info(f"Google API Key present: {'Yes' if google_api_key else 'No'}")
|
| 49 |
logger.info(f"Available datasets: {list(DATASET_CONFIGS.keys())}")
|
| 50 |
|
| 51 |
+
# Define tools for the GLM model
|
| 52 |
+
def rag_qa(question: str, dataset: str = "developer-portfolio") -> str:
|
| 53 |
+
"""
|
| 54 |
+
Get answers from the RAG pipeline for specific questions about the dataset.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
question: The question to answer using the RAG pipeline
|
| 58 |
+
dataset: The dataset to search in (default: developer-portfolio)
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Answer from the RAG pipeline
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
# Check if pipelines are loaded
|
| 65 |
+
if not pipelines:
|
| 66 |
+
return "RAG Pipeline is running but datasets are still loading in the background. Please try again in a moment."
|
| 67 |
+
|
| 68 |
+
# Select the appropriate pipeline based on dataset
|
| 69 |
+
if dataset not in pipelines:
|
| 70 |
+
return f"Dataset '{dataset}' not available. Available datasets: {list(pipelines.keys())}"
|
| 71 |
+
|
| 72 |
+
selected_pipeline = pipelines[dataset]
|
| 73 |
+
answer = selected_pipeline.answer_question(question)
|
| 74 |
+
return answer
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return f"Error accessing RAG pipeline: {str(e)}"
|
| 77 |
+
|
| 78 |
+
# Tool definitions for GLM
|
| 79 |
+
TOOLS = [
|
| 80 |
+
{
|
| 81 |
+
"type": "function",
|
| 82 |
+
"function": {
|
| 83 |
+
"name": "rag_qa",
|
| 84 |
+
"description": "Get answers from the RAG pipeline for specific questions about datasets",
|
| 85 |
+
"parameters": {
|
| 86 |
+
"type": "object",
|
| 87 |
+
"properties": {
|
| 88 |
+
"question": {
|
| 89 |
+
"type": "string",
|
| 90 |
+
"description": "The question to answer using the RAG pipeline"
|
| 91 |
+
},
|
| 92 |
+
"dataset": {
|
| 93 |
+
"type": "string",
|
| 94 |
+
"description": "The dataset to search in (default: developer-portfolio)",
|
| 95 |
+
"default": "developer-portfolio"
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
"required": ["question"]
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
# Don't load datasets during startup - do it asynchronously after server starts
|
| 105 |
logger.info("RAG Pipeline API is ready to serve requests - datasets will load in background")
|
| 106 |
|
|
|
|
| 112 |
text: str
|
| 113 |
dataset: str = "developer-portfolio" # Default dataset
|
| 114 |
|
| 115 |
+
class ChatMessage(BaseModel):
|
| 116 |
+
role: str
|
| 117 |
+
content: str
|
| 118 |
+
|
| 119 |
+
class ChatRequest(BaseModel):
|
| 120 |
+
messages: list[ChatMessage]
|
| 121 |
+
dataset: str = "developer-portfolio" # Default dataset
|
| 122 |
+
|
| 123 |
+
@app.post("/chat")
|
| 124 |
+
async def chat_with_ai(request: ChatRequest):
|
| 125 |
+
"""
|
| 126 |
+
Chat with the AI assistant. The AI will use the RAG pipeline when needed to answer questions about the datasets.
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
# Convert messages to OpenAI format with proper typing
|
| 130 |
+
messages: list[ChatCompletionMessageParam] = [
|
| 131 |
+
{"role": msg.role, "content": msg.content} # type: ignore
|
| 132 |
+
for msg in request.messages
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
# Add system message to guide the AI
|
| 136 |
+
system_message: ChatCompletionMessageParam = {
|
| 137 |
+
"role": "system",
|
| 138 |
+
"content": "You are a helpful AI assistant. You have access to a RAG (Retrieval-Augmented Generation) pipeline that can answer questions about specific datasets. Use the rag_qa tool when users ask questions that would benefit from searching the dataset knowledge. For general conversation, respond normally. The available datasets are primarily focused on developer portfolio information, but can include other topics depending on what's loaded."
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
# Insert system message at the beginning
|
| 142 |
+
messages.insert(0, system_message)
|
| 143 |
+
|
| 144 |
+
# Make the API call with tools
|
| 145 |
+
response = openrouter_client.chat.completions.create(
|
| 146 |
+
model=MODEL_NAME,
|
| 147 |
+
messages=messages,
|
| 148 |
+
tools=TOOLS, # type: ignore
|
| 149 |
+
tool_choice="auto"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
message = response.choices[0].message
|
| 153 |
+
finish_reason = response.choices[0].finish_reason
|
| 154 |
+
|
| 155 |
+
# Handle tool calls
|
| 156 |
+
if finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls:
|
| 157 |
+
tool_results = []
|
| 158 |
+
|
| 159 |
+
# Execute tool calls
|
| 160 |
+
for tool_call in message.tool_calls:
|
| 161 |
+
if tool_call.function.name == "rag_qa":
|
| 162 |
+
# Parse arguments
|
| 163 |
+
args = json.loads(tool_call.function.arguments)
|
| 164 |
+
question = args.get("question")
|
| 165 |
+
dataset = args.get("dataset", request.dataset)
|
| 166 |
+
|
| 167 |
+
# Call the rag_qa function
|
| 168 |
+
result = rag_qa(question, dataset)
|
| 169 |
+
tool_results.append({
|
| 170 |
+
"tool_call_id": tool_call.id,
|
| 171 |
+
"result": result
|
| 172 |
+
})
|
| 173 |
+
|
| 174 |
+
# Add tool results to conversation and get final response
|
| 175 |
+
assistant_message: ChatCompletionMessageParam = {
|
| 176 |
+
"role": "assistant",
|
| 177 |
+
"content": message.content or "",
|
| 178 |
+
"tool_calls": [
|
| 179 |
+
{
|
| 180 |
+
"id": tc.id,
|
| 181 |
+
"type": tc.type,
|
| 182 |
+
"function": {
|
| 183 |
+
"name": tc.function.name,
|
| 184 |
+
"arguments": tc.function.arguments
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
for tc in message.tool_calls
|
| 188 |
+
]
|
| 189 |
+
}
|
| 190 |
+
messages.append(assistant_message)
|
| 191 |
+
|
| 192 |
+
for tool_result in tool_results:
|
| 193 |
+
tool_message: ChatCompletionMessageParam = {
|
| 194 |
+
"role": "tool",
|
| 195 |
+
"tool_call_id": tool_result["tool_call_id"],
|
| 196 |
+
"content": tool_result["result"]
|
| 197 |
+
}
|
| 198 |
+
messages.append(tool_message)
|
| 199 |
+
|
| 200 |
+
# Get final response
|
| 201 |
+
final_response = openrouter_client.chat.completions.create(
|
| 202 |
+
model=MODEL_NAME,
|
| 203 |
+
messages=messages
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
return {
|
| 207 |
+
"response": final_response.choices[0].message.content,
|
| 208 |
+
"tool_calls": [
|
| 209 |
+
{
|
| 210 |
+
"name": tc.function.name,
|
| 211 |
+
"arguments": tc.function.arguments,
|
| 212 |
+
"result": next(tr["result"] for tr in tool_results if tr["tool_call_id"] == tc.id)
|
| 213 |
+
}
|
| 214 |
+
for tc in message.tool_calls
|
| 215 |
+
] if message.tool_calls else None
|
| 216 |
+
}
|
| 217 |
+
else:
|
| 218 |
+
# Direct response without tool calls
|
| 219 |
+
return {
|
| 220 |
+
"response": message.content,
|
| 221 |
+
"tool_calls": None
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
except Exception as e:
|
| 225 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 226 |
+
|
| 227 |
@app.post("/answer")
|
| 228 |
async def get_answer(question: Question):
|
| 229 |
try:
|
|
|
|
| 263 |
async def load_datasets_background():
|
| 264 |
"""Load datasets in background after server starts"""
|
| 265 |
global pipelines
|
| 266 |
+
# Import RAGPipeline only when needed
|
| 267 |
+
from .pipeline import RAGPipeline
|
| 268 |
+
# Only load developer-portfolio to save memory
|
| 269 |
+
dataset_name = "developer-portfolio"
|
| 270 |
+
try:
|
| 271 |
+
logger.info(f"Loading dataset: {dataset_name}")
|
| 272 |
+
pipeline = RAGPipeline.from_preset(preset_name=dataset_name)
|
| 273 |
+
pipelines[dataset_name] = pipeline
|
| 274 |
+
logger.info(f"Successfully loaded {dataset_name}")
|
| 275 |
+
except Exception as e:
|
| 276 |
+
logger.error(f"Failed to load {dataset_name}: {e}")
|
| 277 |
+
logger.info(f"Background loading complete - {len(pipelines)} datasets loaded")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
@app.on_event("startup")
|
| 280 |
async def startup_event():
|
app/pipeline.py
CHANGED
|
@@ -2,8 +2,7 @@ from haystack import Document, Pipeline
|
|
| 2 |
from haystack.document_stores.in_memory import InMemoryDocumentStore
|
| 3 |
from haystack.components.embedders import SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder
|
| 4 |
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
|
| 5 |
-
from haystack.components.builders import
|
| 6 |
-
from haystack_integrations.components.generators.google_ai import GoogleAIGeminiChatGenerator
|
| 7 |
from datasets import load_dataset
|
| 8 |
from haystack.dataclasses import ChatMessage
|
| 9 |
from typing import Optional, List, Union, Dict
|
|
@@ -12,21 +11,17 @@ from .config import DatasetConfig, DATASET_CONFIGS, MODEL_CONFIG
|
|
| 12 |
class RAGPipeline:
|
| 13 |
def __init__(
|
| 14 |
self,
|
| 15 |
-
google_api_key: str,
|
| 16 |
dataset_config: Union[str, DatasetConfig],
|
| 17 |
documents: Optional[List[Union[str, Document]]] = None,
|
| 18 |
-
embedding_model: Optional[str] = None
|
| 19 |
-
llm_model: Optional[str] = None
|
| 20 |
):
|
| 21 |
"""
|
| 22 |
Initialize the RAG Pipeline.
|
| 23 |
|
| 24 |
Args:
|
| 25 |
-
google_api_key: API key for Google AI services
|
| 26 |
dataset_config: Either a string key from DATASET_CONFIGS or a DatasetConfig object
|
| 27 |
documents: Optional list of documents to use instead of loading from a dataset
|
| 28 |
embedding_model: Optional override for embedding model
|
| 29 |
-
llm_model: Optional override for LLM model
|
| 30 |
"""
|
| 31 |
# Load configuration
|
| 32 |
if isinstance(dataset_config, str):
|
|
@@ -74,19 +69,22 @@ class RAGPipeline:
|
|
| 74 |
)
|
| 75 |
self.retriever = InMemoryEmbeddingRetriever(self.document_store)
|
| 76 |
|
| 77 |
-
# Warm up the
|
| 78 |
self.doc_embedder.warm_up()
|
|
|
|
| 79 |
|
| 80 |
# Initialize prompt template
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
|
| 91 |
# Index documents
|
| 92 |
self._index_documents(self.documents)
|
|
@@ -95,15 +93,14 @@ class RAGPipeline:
|
|
| 95 |
self.pipeline = self._build_pipeline()
|
| 96 |
|
| 97 |
@classmethod
|
| 98 |
-
def from_preset(cls,
|
| 99 |
"""
|
| 100 |
Create a pipeline from a preset configuration.
|
| 101 |
|
| 102 |
Args:
|
| 103 |
-
google_api_key: API key for Google AI services
|
| 104 |
preset_name: Name of the preset configuration to use
|
| 105 |
"""
|
| 106 |
-
return cls(
|
| 107 |
|
| 108 |
def _index_documents(self, documents):
|
| 109 |
# Embed and index documents
|
|
@@ -115,19 +112,24 @@ class RAGPipeline:
|
|
| 115 |
pipeline.add_component("text_embedder", self.text_embedder)
|
| 116 |
pipeline.add_component("retriever", self.retriever)
|
| 117 |
pipeline.add_component("prompt_builder", self.prompt_builder)
|
| 118 |
-
pipeline.add_component("llm", self.generator)
|
| 119 |
|
| 120 |
# Connect components
|
| 121 |
pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
|
| 122 |
pipeline.connect("retriever", "prompt_builder")
|
| 123 |
-
pipeline.connect("prompt_builder.prompt", "llm.messages")
|
| 124 |
|
| 125 |
return pipeline
|
| 126 |
|
| 127 |
def answer_question(self, question: str) -> str:
|
| 128 |
"""Run the RAG pipeline to answer a question"""
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from haystack.document_stores.in_memory import InMemoryDocumentStore
|
| 3 |
from haystack.components.embedders import SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder
|
| 4 |
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
|
| 5 |
+
from haystack.components.builders import PromptBuilder
|
|
|
|
| 6 |
from datasets import load_dataset
|
| 7 |
from haystack.dataclasses import ChatMessage
|
| 8 |
from typing import Optional, List, Union, Dict
|
|
|
|
| 11 |
class RAGPipeline:
|
| 12 |
def __init__(
|
| 13 |
self,
|
|
|
|
| 14 |
dataset_config: Union[str, DatasetConfig],
|
| 15 |
documents: Optional[List[Union[str, Document]]] = None,
|
| 16 |
+
embedding_model: Optional[str] = None
|
|
|
|
| 17 |
):
|
| 18 |
"""
|
| 19 |
Initialize the RAG Pipeline.
|
| 20 |
|
| 21 |
Args:
|
|
|
|
| 22 |
dataset_config: Either a string key from DATASET_CONFIGS or a DatasetConfig object
|
| 23 |
documents: Optional list of documents to use instead of loading from a dataset
|
| 24 |
embedding_model: Optional override for embedding model
|
|
|
|
| 25 |
"""
|
| 26 |
# Load configuration
|
| 27 |
if isinstance(dataset_config, str):
|
|
|
|
| 69 |
)
|
| 70 |
self.retriever = InMemoryEmbeddingRetriever(self.document_store)
|
| 71 |
|
| 72 |
+
# Warm up the embedders
|
| 73 |
self.doc_embedder.warm_up()
|
| 74 |
+
self.text_embedder.warm_up()
|
| 75 |
|
| 76 |
# Initialize prompt template
|
| 77 |
+
self.prompt_builder = PromptBuilder(template=self.config.prompt_template or """
|
| 78 |
+
Given the following context, please answer the question.
|
| 79 |
+
|
| 80 |
+
Context:
|
| 81 |
+
{% for document in documents %}
|
| 82 |
+
{{ document.content }}
|
| 83 |
+
{% endfor %}
|
| 84 |
+
|
| 85 |
+
Question: {{question}}
|
| 86 |
+
Answer:
|
| 87 |
+
""")
|
| 88 |
|
| 89 |
# Index documents
|
| 90 |
self._index_documents(self.documents)
|
|
|
|
| 93 |
self.pipeline = self._build_pipeline()
|
| 94 |
|
| 95 |
@classmethod
|
| 96 |
+
def from_preset(cls, preset_name: str):
|
| 97 |
"""
|
| 98 |
Create a pipeline from a preset configuration.
|
| 99 |
|
| 100 |
Args:
|
|
|
|
| 101 |
preset_name: Name of the preset configuration to use
|
| 102 |
"""
|
| 103 |
+
return cls(dataset_config=preset_name)
|
| 104 |
|
| 105 |
def _index_documents(self, documents):
|
| 106 |
# Embed and index documents
|
|
|
|
| 112 |
pipeline.add_component("text_embedder", self.text_embedder)
|
| 113 |
pipeline.add_component("retriever", self.retriever)
|
| 114 |
pipeline.add_component("prompt_builder", self.prompt_builder)
|
|
|
|
| 115 |
|
| 116 |
# Connect components
|
| 117 |
pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
|
| 118 |
pipeline.connect("retriever", "prompt_builder")
|
|
|
|
| 119 |
|
| 120 |
return pipeline
|
| 121 |
|
| 122 |
def answer_question(self, question: str) -> str:
|
| 123 |
"""Run the RAG pipeline to answer a question"""
|
| 124 |
+
# First, embed the question and retrieve relevant documents
|
| 125 |
+
embedded_question = self.text_embedder.run(text=question)
|
| 126 |
+
retrieved_docs = self.retriever.run(query_embedding=embedded_question["embedding"])
|
| 127 |
+
|
| 128 |
+
# Then, build the prompt with retrieved documents
|
| 129 |
+
prompt_result = self.prompt_builder.run(
|
| 130 |
+
question=question,
|
| 131 |
+
documents=retrieved_docs["documents"]
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Return the formatted prompt (this will be processed by the main AI)
|
| 135 |
+
return prompt_result["prompt"]
|
test_openrouter_connection.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for OpenRouter API connection with z-ai/glm-4.5-air:free model.
|
| 4 |
+
Tests basic functionality and tool calling capabilities.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
|
| 12 |
+
# Model configuration
|
| 13 |
+
MODEL_NAME = "z-ai/glm-4.5-air:free"
|
| 14 |
+
|
| 15 |
+
def test_basic_connection():
|
| 16 |
+
"""Test basic API connection with a simple prompt."""
|
| 17 |
+
print("=" * 60)
|
| 18 |
+
print("Testing Basic OpenRouter Connection")
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
# Initialize OpenRouter client with the same configuration as app.py
|
| 23 |
+
client = OpenAI(
|
| 24 |
+
base_url="https://openrouter.ai/api/v1",
|
| 25 |
+
api_key="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"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Test with a simple prompt
|
| 29 |
+
messages = [
|
| 30 |
+
{"role": "user", "content": "Hello! Please respond with a simple greeting and your name."}
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
print("Sending test request to OpenRouter API...")
|
| 34 |
+
response = client.chat.completions.create(
|
| 35 |
+
model=MODEL_NAME,
|
| 36 |
+
messages=messages
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Extract and display the response
|
| 40 |
+
content = response.choices[0].message.content
|
| 41 |
+
print(f"β
SUCCESS: API connection works!")
|
| 42 |
+
print(f"Model: {response.model}")
|
| 43 |
+
print(f"Response: {content}")
|
| 44 |
+
print(f"Usage: {response.usage}")
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"β FAILED: Basic connection test failed")
|
| 49 |
+
print(f"Error: {str(e)}")
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
def test_tool_calling():
|
| 53 |
+
"""Test tool calling functionality."""
|
| 54 |
+
print("\n" + "=" * 60)
|
| 55 |
+
print("Testing Tool Calling Functionality")
|
| 56 |
+
print("=" * 60)
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
# Initialize OpenRouter client
|
| 60 |
+
client = OpenAI(
|
| 61 |
+
base_url="https://openrouter.ai/api/v1",
|
| 62 |
+
api_key="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"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Define test tools (similar to app.py)
|
| 66 |
+
tools = [
|
| 67 |
+
{
|
| 68 |
+
"type": "function",
|
| 69 |
+
"function": {
|
| 70 |
+
"name": "get_weather",
|
| 71 |
+
"description": "Get current weather information",
|
| 72 |
+
"parameters": {
|
| 73 |
+
"type": "object",
|
| 74 |
+
"properties": {
|
| 75 |
+
"location": {
|
| 76 |
+
"type": "string",
|
| 77 |
+
"description": "The city name for weather information"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"required": ["location"]
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
# Test prompt that should trigger tool calling
|
| 87 |
+
messages = [
|
| 88 |
+
{"role": "user", "content": "What's the weather like in New York?"}
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
print("Sending test request with tool calling capability...")
|
| 92 |
+
response = client.chat.completions.create(
|
| 93 |
+
model=MODEL_NAME,
|
| 94 |
+
messages=messages,
|
| 95 |
+
tools=tools
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Analyze the response
|
| 99 |
+
finish_reason = response.choices[0].finish_reason
|
| 100 |
+
message = response.choices[0].message
|
| 101 |
+
|
| 102 |
+
print(f"β
SUCCESS: Tool calling test completed!")
|
| 103 |
+
print(f"Model: {response.model}")
|
| 104 |
+
print(f"Finish Reason: {finish_reason}")
|
| 105 |
+
|
| 106 |
+
if finish_reason == "tool_calls":
|
| 107 |
+
print("π§ Tool calls detected:")
|
| 108 |
+
if hasattr(message, 'tool_calls') and message.tool_calls:
|
| 109 |
+
for tool_call in message.tool_calls:
|
| 110 |
+
print(f" - Tool: {tool_call.function.name}")
|
| 111 |
+
print(f" - Arguments: {tool_call.function.arguments}")
|
| 112 |
+
else:
|
| 113 |
+
print(" - No tool calls found in response")
|
| 114 |
+
else:
|
| 115 |
+
print(f" - Response content: {message.content}")
|
| 116 |
+
|
| 117 |
+
print(f"Usage: {response.usage}")
|
| 118 |
+
return True
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"β FAILED: Tool calling test failed")
|
| 122 |
+
print(f"Error: {str(e)}")
|
| 123 |
+
return False
|
| 124 |
+
|
| 125 |
+
def test_error_handling():
|
| 126 |
+
"""Test error handling with invalid requests."""
|
| 127 |
+
print("\n" + "=" * 60)
|
| 128 |
+
print("Testing Error Handling")
|
| 129 |
+
print("=" * 60)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
# Initialize OpenRouter client
|
| 133 |
+
client = OpenAI(
|
| 134 |
+
base_url="https://openrouter.ai/api/v1",
|
| 135 |
+
api_key="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"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Test with empty messages
|
| 139 |
+
print("Testing empty messages...")
|
| 140 |
+
try:
|
| 141 |
+
response = client.chat.completions.create(
|
| 142 |
+
model="z-ai/glm-4.5-air:free",
|
| 143 |
+
messages=[]
|
| 144 |
+
)
|
| 145 |
+
print("β οΈ Unexpected: Empty messages request succeeded")
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"β
Expected error caught: {str(e)}")
|
| 148 |
+
|
| 149 |
+
# Test with invalid model
|
| 150 |
+
print("Testing invalid model...")
|
| 151 |
+
try:
|
| 152 |
+
response = client.chat.completions.create(
|
| 153 |
+
model="invalid-model-name",
|
| 154 |
+
messages=[{"role": "user", "content": "Hello"}]
|
| 155 |
+
)
|
| 156 |
+
print("β οΈ Unexpected: Invalid model request succeeded")
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"β
Expected error caught: {str(e)}")
|
| 159 |
+
|
| 160 |
+
print("β
SUCCESS: Error handling tests completed")
|
| 161 |
+
return True
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
print(f"β FAILED: Error handling test failed")
|
| 165 |
+
print(f"Error: {str(e)}")
|
| 166 |
+
return False
|
| 167 |
+
|
| 168 |
+
def test_conversation_flow():
|
| 169 |
+
"""Test a multi-turn conversation."""
|
| 170 |
+
print("\n" + "=" * 60)
|
| 171 |
+
print("Testing Multi-turn Conversation")
|
| 172 |
+
print("=" * 60)
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
# Initialize OpenRouter client
|
| 176 |
+
client = OpenAI(
|
| 177 |
+
base_url="https://openrouter.ai/api/v1",
|
| 178 |
+
api_key="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"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Simulate a conversation
|
| 182 |
+
messages = [
|
| 183 |
+
{"role": "user", "content": "Hello! Can you help me understand what AI is?"}
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
print("Starting conversation flow...")
|
| 187 |
+
|
| 188 |
+
# First turn
|
| 189 |
+
response = client.chat.completions.create(
|
| 190 |
+
model=MODEL_NAME,
|
| 191 |
+
messages=messages
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
content = response.choices[0].message.content
|
| 195 |
+
print(f"Assistant: {content}")
|
| 196 |
+
|
| 197 |
+
# Second turn
|
| 198 |
+
messages.append({"role": "assistant", "content": content})
|
| 199 |
+
messages.append({"role": "user", "content": "Can you give me a simple example?"})
|
| 200 |
+
|
| 201 |
+
response = client.chat.completions.create(
|
| 202 |
+
model=MODEL_NAME,
|
| 203 |
+
messages=messages
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
content = response.choices[0].message.content
|
| 207 |
+
print(f"Assistant: {content}")
|
| 208 |
+
|
| 209 |
+
print("β
SUCCESS: Multi-turn conversation completed")
|
| 210 |
+
return True
|
| 211 |
+
|
| 212 |
+
except Exception as e:
|
| 213 |
+
print(f"β FAILED: Conversation flow test failed")
|
| 214 |
+
print(f"Error: {str(e)}")
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
def main():
|
| 218 |
+
"""Main test function."""
|
| 219 |
+
print("π Starting OpenRouter API Connection Tests")
|
| 220 |
+
print(f"Model: {MODEL_NAME}")
|
| 221 |
+
print(f"API Base URL: https://openrouter.ai/api/v1")
|
| 222 |
+
|
| 223 |
+
# Run all tests
|
| 224 |
+
tests = [
|
| 225 |
+
("Basic Connection", test_basic_connection),
|
| 226 |
+
("Tool Calling", test_tool_calling),
|
| 227 |
+
("Error Handling", test_error_handling),
|
| 228 |
+
("Conversation Flow", test_conversation_flow)
|
| 229 |
+
]
|
| 230 |
+
|
| 231 |
+
results = []
|
| 232 |
+
for test_name, test_func in tests:
|
| 233 |
+
try:
|
| 234 |
+
result = test_func()
|
| 235 |
+
results.append((test_name, result))
|
| 236 |
+
except Exception as e:
|
| 237 |
+
print(f"β CRITICAL ERROR in {test_name}: {str(e)}")
|
| 238 |
+
results.append((test_name, False))
|
| 239 |
+
|
| 240 |
+
# Summary
|
| 241 |
+
print("\n" + "=" * 60)
|
| 242 |
+
print("TEST SUMMARY")
|
| 243 |
+
print("=" * 60)
|
| 244 |
+
|
| 245 |
+
passed = 0
|
| 246 |
+
total = len(results)
|
| 247 |
+
|
| 248 |
+
for test_name, result in results:
|
| 249 |
+
status = "β
PASSED" if result else "β FAILED"
|
| 250 |
+
print(f"{status}: {test_name}")
|
| 251 |
+
if result:
|
| 252 |
+
passed += 1
|
| 253 |
+
|
| 254 |
+
print(f"\nOverall: {passed}/{total} tests passed")
|
| 255 |
+
|
| 256 |
+
if passed == total:
|
| 257 |
+
print("π All tests passed! OpenRouter integration is working correctly.")
|
| 258 |
+
return 0
|
| 259 |
+
else:
|
| 260 |
+
print("β οΈ Some tests failed. Please check the configuration and API credentials.")
|
| 261 |
+
return 1
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
sys.exit(main())
|