Quincy Hsieh commited on
Commit
4cf3b63
·
1 Parent(s): c8361c5

Call LLM model from Azure OpenAI endpoint

Browse files
Files changed (5) hide show
  1. README.md +12 -8
  2. app.py +50 -34
  3. config.json +9 -0
  4. prompts/rag_prompt.txt +2 -2
  5. requirements.txt +1 -0
README.md CHANGED
@@ -59,7 +59,7 @@ This application demonstrates how to build a production-ready RAG system within
59
  │ ┌──────────────────┐ ┌─────────────────┐ │
60
  │ │ HF Inference │ │ Sentence │ │
61
  │ │ API (LLM) │ │ Transformers │ │
62
- │ │ Mistral-7B │ │ (Embeddings) │ │
63
  │ └──────────────────┘ └─────────────────┘ │
64
  └─────────────────────────────────────────────────────────────┘
65
  ```
@@ -144,30 +144,34 @@ results = collection.query(
144
  Combine retrieved context with the user's question and generate an answer.
145
 
146
  1. **Build prompt** — Load the template from [`prompts/rag_prompt.txt`](prompts/rag_prompt.txt), inject retrieved context and the user's question
147
- 2. **Call LLM API** — Send the prompt to the Hugging Face Inference API (Mistral-7B-Instruct-v0.3)
148
  3. **Return response** — The LLM generates an answer grounded in the provided context
149
 
150
  The prompt template (`prompts/rag_prompt.txt`):
151
 
152
  ```
153
- <s>[INST] You are a helpful assistant. Answer the user's question based ONLY on the provided context.
154
  If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
155
  Always be concise and factual.
156
 
157
  Context:
158
  {context}
159
 
160
- Question: {question} [/INST]
161
  ```
162
 
163
- The template is loaded once at startup and filled at query time:
164
 
165
  ```python
166
  RAG_PROMPT_TEMPLATE = Path("prompts/rag_prompt.txt").read_text(encoding="utf-8")
167
 
168
  # At query time:
169
  prompt = RAG_PROMPT_TEMPLATE.format(context=context_text, question=user_query)
170
- response = client.text_generation(prompt, max_new_tokens=512)
 
 
 
 
171
  ```
172
 
173
  > **Tip:** Edit `prompts/rag_prompt.txt` to tune the model's behaviour (tone, language, output format) without touching application code.
@@ -335,7 +339,7 @@ curl -X POST http://localhost:7860/ingest \
335
  |---|---|---|
336
  | `HF_TOKEN` | (Space Secret) | Hugging Face API token for Inference API |
337
  | `EMBEDDING_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Model for text embeddings |
338
- | `LLM_MODEL_NAME` | `mistralai/Mistral-7B-Instruct-v0.3` | LLM for answer generation |
339
  | `CHROMA_PERSIST_DIR` | `./chroma_db` | ChromaDB storage path |
340
  | `CHUNK_SIZE` | `512` | Text chunk size in characters |
341
  | `CHUNK_OVERLAP` | `50` | Overlap between chunks |
@@ -384,7 +388,7 @@ If you see `capture() takes 1 positional argument but 3 were given` in the logs,
384
 
385
  ### LLM 404 — Model Not Found
386
 
387
- If the `/query` endpoint logs a `404 Not Found` for the Inference API URL, the configured model has been removed from the free serverless tier. Update `LLM_MODEL_NAME` in `app.py` to an available model and adjust `prompts/rag_prompt.txt` to match its instruction format. The current default (`mistralai/Mistral-7B-Instruct-v0.3`) uses the `<s>[INST] [/INST]` format.
388
 
389
  ### SSL Certificate Verification Error
390
 
 
59
  │ ┌──────────────────┐ ┌─────────────────┐ │
60
  │ │ HF Inference │ │ Sentence │ │
61
  │ │ API (LLM) │ │ Transformers │ │
62
+ │ │ Qwen2.5-72B │ │ (Embeddings) │ │
63
  │ └──────────────────┘ └─────────────────┘ │
64
  └─────────────────────────────────────────────────────────────┘
65
  ```
 
144
  Combine retrieved context with the user's question and generate an answer.
145
 
146
  1. **Build prompt** — Load the template from [`prompts/rag_prompt.txt`](prompts/rag_prompt.txt), inject retrieved context and the user's question
147
+ 2. **Call LLM API** — Send the prompt to the Hugging Face Inference API (`chat_completion`, Qwen2.5-72B-Instruct)
148
  3. **Return response** — The LLM generates an answer grounded in the provided context
149
 
150
  The prompt template (`prompts/rag_prompt.txt`):
151
 
152
  ```
153
+ You are a helpful assistant. Answer the user's question based ONLY on the provided context.
154
  If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
155
  Always be concise and factual.
156
 
157
  Context:
158
  {context}
159
 
160
+ Question: {question}
161
  ```
162
 
163
+ The template is loaded once at startup and sent as the user message to the chat endpoint:
164
 
165
  ```python
166
  RAG_PROMPT_TEMPLATE = Path("prompts/rag_prompt.txt").read_text(encoding="utf-8")
167
 
168
  # At query time:
169
  prompt = RAG_PROMPT_TEMPLATE.format(context=context_text, question=user_query)
170
+ response = llm_client.chat_completion(
171
+ messages=[{"role": "user", "content": prompt}],
172
+ max_tokens=512,
173
+ )
174
+ answer = response.choices[0].message.content
175
  ```
176
 
177
  > **Tip:** Edit `prompts/rag_prompt.txt` to tune the model's behaviour (tone, language, output format) without touching application code.
 
339
  |---|---|---|
340
  | `HF_TOKEN` | (Space Secret) | Hugging Face API token for Inference API |
341
  | `EMBEDDING_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Model for text embeddings |
342
+ | `LLM_MODEL_NAME` | `Qwen/Qwen2.5-72B-Instruct` | LLM for answer generation |
343
  | `CHROMA_PERSIST_DIR` | `./chroma_db` | ChromaDB storage path |
344
  | `CHUNK_SIZE` | `512` | Text chunk size in characters |
345
  | `CHUNK_OVERLAP` | `50` | Overlap between chunks |
 
388
 
389
  ### LLM 404 — Model Not Found
390
 
391
+ If the `/query` endpoint logs a `404 Not Found` for the Inference API URL, the configured model has been removed from the free serverless tier. Update `LLM_MODEL_NAME` in `app.py` to an available model. The app now uses `chat_completion` (`/v1/chat/completions`) rather than the legacy `text_generation` endpoint, so any model listed under **Inference Providers** on huggingface.co will work. The prompt in `prompts/rag_prompt.txt` is model-agnostic (no special tokens).
392
 
393
  ### SSL Certificate Verification Error
394
 
app.py CHANGED
@@ -16,10 +16,16 @@ Architecture:
16
  """
17
 
18
  import os
 
19
  import logging
20
  from pathlib import Path
21
  from typing import Optional
22
 
 
 
 
 
 
23
  import gradio as gr
24
  from fastapi import FastAPI, HTTPException
25
  from fastapi.responses import JSONResponse
@@ -27,38 +33,41 @@ from pydantic import BaseModel
27
  import chromadb
28
  from chromadb.config import Settings
29
  from sentence_transformers import SentenceTransformer
30
- from huggingface_hub import InferenceClient
31
  from langchain_text_splitters import RecursiveCharacterTextSplitter
32
  from pypdf import PdfReader
33
 
34
  logging.basicConfig(level=logging.INFO)
35
  logger = logging.getLogger(__name__)
36
 
 
 
 
37
  # ---------------------------------------------------------------------------
38
  # Configuration
39
  # ---------------------------------------------------------------------------
40
 
41
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
42
- LLM_MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.3"
43
  CHROMA_PERSIST_DIR = "./chroma_db"
44
  COLLECTION_NAME = "rag_documents"
45
  CHUNK_SIZE = 512
46
  CHUNK_OVERLAP = 50
47
  TOP_K_RESULTS = 3
48
 
49
- # HF_TOKEN is set as a Space secret for authenticated Inference API calls
50
- HF_TOKEN = os.environ.get("HF_TOKEN")
 
 
 
 
 
 
 
 
51
 
52
- # Corporate proxy / SSL inspection support:
53
- # If HTTPS requests to huggingface.co fail with certificate errors, point this
54
- # variable to your organisation's CA bundle before starting the app.
55
- # export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Linux system bundle
56
- # export REQUESTS_CA_BUNDLE=/path/to/corporate-ca.crt # custom bundle
57
- # The requests library (used by huggingface_hub and sentence-transformers) reads
58
- # this variable automatically — no code change required.
59
- _ca_bundle = os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE")
60
- if _ca_bundle:
61
- logger.info(f"Using custom CA bundle for HTTPS: {_ca_bundle}")
62
 
63
  # Prompt template loaded from file so it can be edited without touching application code
64
  _PROMPT_TEMPLATE_PATH = Path(__file__).parent / "prompts" / "rag_prompt.txt"
@@ -92,13 +101,12 @@ collection = chroma_client.get_or_create_collection(
92
  logger.info(f"ChromaDB collection '{COLLECTION_NAME}' ready. Documents: {collection.count()}")
93
 
94
  # ---------------------------------------------------------------------------
95
- # Step 3: Initialize the LLM Client (Hugging Face Inference API)
96
  # ---------------------------------------------------------------------------
97
- # We use the Hugging Face Inference API to call a hosted LLM model.
98
- # This avoids loading large models into the Space's limited memory.
99
 
100
- llm_client = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN)
101
- logger.info(f"LLM client initialized for model: {LLM_MODEL_NAME}")
102
 
103
 
104
  # ---------------------------------------------------------------------------
@@ -215,25 +223,33 @@ def retrieve_relevant_context(query: str, top_k: int = TOP_K_RESULTS) -> list[di
215
 
216
  def call_llm(prompt: str) -> str:
217
  """
218
- Make a call to the LLM via Hugging Face Inference API.
219
 
220
- This demonstrates Step 2a: How to call a given LLM model API.
221
- We use the text_generation endpoint with a structured prompt
222
- that includes the retrieved context and the user's question.
223
  """
 
 
 
 
 
 
 
 
 
 
 
224
  try:
225
- response = llm_client.text_generation(
226
- prompt,
227
- max_new_tokens=512,
228
- temperature=0.7,
229
- top_p=0.95,
230
- repetition_penalty=1.1,
231
- do_sample=True,
232
- )
233
- return response.strip()
234
- except Exception as e:
235
- logger.error(f"LLM API call failed: {e}")
236
  raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
 
 
 
237
 
238
 
239
  def build_rag_prompt(query: str, contexts: list[dict]) -> str:
 
16
  """
17
 
18
  import os
19
+ import json
20
  import logging
21
  from pathlib import Path
22
  from typing import Optional
23
 
24
+ # Must be set before chromadb is imported so the module never registers its
25
+ # posthog telemetry hook (workaround for posthog v3 API incompatibility).
26
+ os.environ.setdefault("ANONYMIZED_TELEMETRY", "False")
27
+
28
+ import requests as http_requests
29
  import gradio as gr
30
  from fastapi import FastAPI, HTTPException
31
  from fastapi.responses import JSONResponse
 
33
  import chromadb
34
  from chromadb.config import Settings
35
  from sentence_transformers import SentenceTransformer
 
36
  from langchain_text_splitters import RecursiveCharacterTextSplitter
37
  from pypdf import PdfReader
38
 
39
  logging.basicConfig(level=logging.INFO)
40
  logger = logging.getLogger(__name__)
41
 
42
+ # Suppress non-fatal chromadb telemetry errors (posthog v3 API incompatibility)
43
+ logging.getLogger("chromadb.telemetry.product.posthog").setLevel(logging.CRITICAL)
44
+
45
  # ---------------------------------------------------------------------------
46
  # Configuration
47
  # ---------------------------------------------------------------------------
48
 
49
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
 
50
  CHROMA_PERSIST_DIR = "./chroma_db"
51
  COLLECTION_NAME = "rag_documents"
52
  CHUNK_SIZE = 512
53
  CHUNK_OVERLAP = 50
54
  TOP_K_RESULTS = 3
55
 
56
+ # LLM settings loaded from config.json
57
+ _CONFIG_PATH = Path(__file__).parent / "config.json"
58
+ with open(_CONFIG_PATH, encoding="utf-8") as _f:
59
+ _config = json.load(_f)
60
+
61
+ LLM_ENDPOINT_URL = _config["llm"]["endpoint_url"]
62
+ LLM_MODEL_NAME = _config["llm"]["model"]
63
+ LLM_MAX_TOKENS = _config["llm"].get("max_tokens", 512)
64
+ LLM_TEMPERATURE = _config["llm"].get("temperature", 0.7)
65
+ LLM_TOP_P = _config["llm"].get("top_p", 0.95)
66
 
67
+ # Azure Foundry API key from environment variable
68
+ AZURE_API_KEY = os.environ.get("AZURE_API_KEY")
69
+ if not AZURE_API_KEY:
70
+ logger.warning("AZURE_API_KEY is not set — LLM calls will fail.")
 
 
 
 
 
 
71
 
72
  # Prompt template loaded from file so it can be edited without touching application code
73
  _PROMPT_TEMPLATE_PATH = Path(__file__).parent / "prompts" / "rag_prompt.txt"
 
101
  logger.info(f"ChromaDB collection '{COLLECTION_NAME}' ready. Documents: {collection.count()}")
102
 
103
  # ---------------------------------------------------------------------------
104
+ # Step 3: LLM Configuration (Azure Foundry GPT-5)
105
  # ---------------------------------------------------------------------------
106
+ # We call the Azure OpenAI-compatible endpoint directly via requests.
107
+ # Endpoint URL and model are configured in config.json.
108
 
109
+ logger.info(f"LLM configured: {LLM_MODEL_NAME} via {LLM_ENDPOINT_URL}")
 
110
 
111
 
112
  # ---------------------------------------------------------------------------
 
223
 
224
  def call_llm(prompt: str) -> str:
225
  """
226
+ Make a call to the LLM via Azure Foundry (GPT-5).
227
 
228
+ Calls the Azure OpenAI-compatible chat/completions endpoint.
229
+ Endpoint URL is loaded from config.json; API key from AZURE_API_KEY env var.
 
230
  """
231
+ headers = {
232
+ "api-key": AZURE_API_KEY,
233
+ "Content-Type": "application/json",
234
+ }
235
+ payload = {
236
+ "model": LLM_MODEL_NAME,
237
+ "messages": [{"role": "user", "content": prompt}],
238
+ "max_tokens": LLM_MAX_TOKENS,
239
+ "temperature": LLM_TEMPERATURE,
240
+ "top_p": LLM_TOP_P,
241
+ }
242
  try:
243
+ resp = http_requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload, timeout=60)
244
+ resp.raise_for_status()
245
+ data = resp.json()
246
+ return data["choices"][0]["message"]["content"].strip()
247
+ except http_requests.exceptions.HTTPError as e:
248
+ logger.error(f"LLM API call failed: {e} — {resp.text}")
 
 
 
 
 
249
  raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
250
+ except (KeyError, IndexError) as e:
251
+ logger.error(f"Unexpected LLM response format: {e}")
252
+ raise HTTPException(status_code=502, detail="Unexpected response from LLM service")
253
 
254
 
255
  def build_rag_prompt(query: str, contexts: list[dict]) -> str:
config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "llm": {
3
+ "endpoint_url": "https://<your-resource>.openai.azure.com/openai/deployments/<your-deployment>/chat/completions?api-version=2024-12-01-preview",
4
+ "model": "gpt-5",
5
+ "max_tokens": 512,
6
+ "temperature": 0.7,
7
+ "top_p": 0.95
8
+ }
9
+ }
prompts/rag_prompt.txt CHANGED
@@ -1,9 +1,9 @@
1
- <s>[INST] You are a helpful assistant. Answer the user's question based ONLY on the provided context.
2
  If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
3
  Always be concise and factual.
4
 
5
  Context:
6
  {context}
7
 
8
- Question: {question} [/INST]
9
 
 
1
+ You are a helpful assistant. Answer the user's question based ONLY on the provided context.
2
  If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
3
  Always be concise and factual.
4
 
5
  Context:
6
  {context}
7
 
8
+ Question: {question}
9
 
requirements.txt CHANGED
@@ -12,3 +12,4 @@ langchain-text-splitters==0.3.0
12
  pypdf==4.3.0
13
  python-multipart==0.0.9
14
  pydantic==2.9.0
 
 
12
  pypdf==4.3.0
13
  python-multipart==0.0.9
14
  pydantic==2.9.0
15
+ requests>=2.31.0