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 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 and LLM models
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
- if google_api_key:
90
- # Import RAGPipeline only when needed
91
- from .pipeline import RAGPipeline
92
- # Only load developer-portfolio to save memory
93
- dataset_name = "developer-portfolio"
94
- try:
95
- logger.info(f"Loading dataset: {dataset_name}")
96
- pipeline = RAGPipeline.from_preset(
97
- google_api_key=google_api_key,
98
- preset_name=dataset_name
99
- )
100
- pipelines[dataset_name] = pipeline
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 ChatPromptBuilder
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 document embedder
78
  self.doc_embedder.warm_up()
 
79
 
80
  # Initialize prompt template
81
- template = [
82
- ChatMessage.from_user(self.config.prompt_template)
83
- ]
84
- self.prompt_builder = ChatPromptBuilder(template=template)
85
-
86
- # Initialize the generator
87
- self.generator = GoogleAIGeminiChatGenerator(
88
- model=llm_model or MODEL_CONFIG["llm_model"]
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, google_api_key: str, preset_name: str):
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(google_api_key=google_api_key, dataset_config=preset_name)
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
- result = self.pipeline.run({
130
- "text_embedder": {"text": question},
131
- "prompt_builder": {"question": question}
132
- })
133
- return result["llm"]["replies"][0].text
 
 
 
 
 
 
 
 
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())