Spaces:
Sleeping
Sleeping
| """ | |
| llm.py | |
| ββββββ | |
| Wraps the Groq API client and owns all prompt engineering for AstroBot. | |
| Responsibilities: | |
| - Validate the Groq API key at startup | |
| - Build the system prompt (astrology tutor persona + no-prediction guardrail) | |
| - Format retrieved context chunks into the prompt | |
| - Call the Groq chat completion endpoint and return the answer string | |
| """ | |
| import logging | |
| from groq import Groq | |
| from langchain_core.documents import Document | |
| from config import cfg | |
| logger = logging.getLogger(__name__) | |
| # ββ System prompt βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Defines the bot's persona, scope, and hard guardrails. | |
| SYSTEM_TEMPLATE = """You are AstroBot, a patient and knowledgeable astrology tutor. | |
| Your students are learning astrology concepts. Your role is to: | |
| β’ Explain astrological concepts clearly and accurately using the provided context. | |
| β’ Use analogies and examples to make complex ideas approachable. | |
| β’ Reference classical and modern astrology where relevant. | |
| β’ Encourage curiosity and deeper study. | |
| HARD RULES β never break these: | |
| 1. Do NOT make personal predictions or interpret anyone's birth chart. | |
| 2. Do NOT speculate about future events for specific individuals. | |
| 3. If the context does not contain enough information to answer, say so honestly | |
| and suggest the student consult a textbook or senior practitioner. | |
| 4. Keep answers focused on educational content only. | |
| --- CONTEXT FROM COURSE MATERIALS --- | |
| {context} | |
| --- END OF CONTEXT --- | |
| Answer the student's question based solely on the context above. | |
| If the answer isn't in the context, say: "I don't have that in my course materials right now β | |
| let me point you to further study resources." | |
| """ | |
| # ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def create_client() -> Groq: | |
| """ | |
| Initialise and validate the Groq client. | |
| Raises | |
| ------ | |
| ValueError | |
| If GROQ_API_KEY is missing. | |
| """ | |
| if not cfg.groq_api_key: | |
| raise ValueError( | |
| "GROQ_API_KEY is not set. Add it in Space β Settings β Repository secrets." | |
| ) | |
| logger.info("Groq client initialised (model: %s)", cfg.groq_model) | |
| return Groq(api_key=cfg.groq_api_key) | |
| def generate_answer(client: Groq, query: str, context_docs: list[Document]) -> str: | |
| """ | |
| Build the RAG prompt and call Groq to get an answer. | |
| Parameters | |
| ---------- | |
| client : Groq | |
| Groq client returned by create_client(). | |
| query : str | |
| The student's question. | |
| context_docs : list[Document] | |
| Retrieved chunks from the vector store. | |
| Returns | |
| ------- | |
| str | |
| The model's answer string. | |
| """ | |
| context_text = _format_context(context_docs) | |
| system_prompt = SYSTEM_TEMPLATE.format(context=context_text) | |
| logger.debug("Calling Groq | model=%s | context_chunks=%d", cfg.groq_model, len(context_docs)) | |
| response = client.chat.completions.create( | |
| model=cfg.groq_model, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": query}, | |
| ], | |
| temperature=cfg.groq_temperature, | |
| max_tokens=cfg.groq_max_tokens, | |
| ) | |
| answer = response.choices[0].message.content | |
| logger.debug("Groq response: %d chars", len(answer)) | |
| return answer | |
| # ββ Internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _format_context(docs: list[Document]) -> str: | |
| """ | |
| Format retrieved documents into a numbered context block | |
| that is easy for the LLM to parse. | |
| """ | |
| blocks = [] | |
| for i, doc in enumerate(docs, 1): | |
| source = doc.metadata.get("source", doc.metadata.get("source_row", i)) | |
| page = doc.metadata.get("page", "") | |
| header = f"[Source {i}" + (f" | {source}" if source else "") + (f" | p.{page}" if page else "") + "]" | |
| blocks.append(f"{header}\n{doc.page_content}") | |
| return "\n\n".join(blocks) | |