Spaces:
Running
Running
| """Gemini/DeepSeek-backed expert for concept extraction and student dialogue.""" | |
| import json | |
| import os | |
| EXPERT_SYSTEM_PROMPT = """You are a knowledgeable subject-matter expert on the \ | |
| study material provided below. A student is going to question, probe, and \ | |
| challenge you in order to deepen their own understanding. You are not a tutor \ | |
| who quizzes them; they lead the inquiry. | |
| Rules: | |
| - Answer directly and substantively, grounded in the material. Do not pad. | |
| - Match the student's depth. A plain factual question gets a plain answer; a \ | |
| question that probes mechanism or challenges an assumption gets a deeper, \ | |
| reasoned response. | |
| - When the student challenges you or surfaces a genuine edge case, limitation, \ | |
| or counterexample, acknowledge it honestly and engage with it. Do not be \ | |
| defensive, and do not pretend the material is more complete than it is. | |
| - If they ask something the material does not cover, say so plainly and reason \ | |
| from first principles, flagging that you are going beyond the source. | |
| - Never quiz the student back or end with "does that make sense?". They are the \ | |
| one asking the questions. | |
| - You may use light markdown (bold, lists) when it aids clarity. | |
| Study material: | |
| --- | |
| {material} | |
| --- | |
| """ | |
| CONCEPT_PROMPT = """From the study material below, extract the {n} most \ | |
| important distinct concepts or topics a student would want to understand. \ | |
| Return a JSON object with a single key "concepts" whose value is an array of \ | |
| short concept names (2-5 words each), ordered from most to least central. | |
| Material: | |
| --- | |
| {material} | |
| --- | |
| Return only the JSON object.""" | |
| class Expert: | |
| """Concept extraction and expert dialogue, via DeepSeek or Gemini.""" | |
| def __init__(self, product_config): | |
| """Initialise the client for the configured provider.""" | |
| self.cfg = product_config | |
| self.provider = getattr(product_config, "provider", "deepseek") | |
| if self.provider == "deepseek": | |
| self._init_deepseek() | |
| elif self.provider == "gemini": | |
| self._init_gemini() | |
| else: | |
| raise ValueError(f"Unknown provider: {self.provider}") | |
| def _init_deepseek(self): | |
| """Set up the DeepSeek (OpenAI-compatible) client.""" | |
| from openai import OpenAI | |
| api_key = os.environ.get("DEEPSEEK_API_KEY") | |
| if not api_key: | |
| raise SystemExit( | |
| "DEEPSEEK_API_KEY is not set. Add it to .env, or set " | |
| "ProductConfig.provider = 'gemini' to use Gemini instead." | |
| ) | |
| self.client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com") | |
| self.model = self.cfg.deepseek_model | |
| def _init_gemini(self): | |
| """Set up the Gemini client.""" | |
| from google import genai | |
| api_key = os.environ.get("GEMINI_API_KEY") | |
| if not api_key: | |
| raise SystemExit("GEMINI_API_KEY is not set. Add it to .env.") | |
| self.client = genai.Client(api_key=api_key) | |
| self.model = self.cfg.gemini_model | |
| # ---- concept extraction ---- | |
| def extract_concepts(self, material_text): | |
| """Ask the model for the key concepts in the material.""" | |
| material = material_text[: self.cfg.max_context_chars] | |
| prompt = CONCEPT_PROMPT.format(n=self.cfg.num_concepts, material=material) | |
| if self.provider == "deepseek": | |
| raw = self._deepseek_json(prompt) | |
| else: | |
| raw = self._gemini_json(prompt) | |
| try: | |
| parsed = json.loads(raw) | |
| concepts = parsed.get("concepts", []) if isinstance(parsed, dict) else [] | |
| except (json.JSONDecodeError, TypeError): | |
| concepts = [] | |
| return [c.strip() for c in concepts if isinstance(c, str) and c.strip()] | |
| def _deepseek_json(self, prompt): | |
| """One DeepSeek call returning a JSON object string.""" | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[{"role": "user", "content": prompt}], | |
| response_format={"type": "json_object"}, | |
| temperature=0.3, | |
| ) | |
| return response.choices[0].message.content | |
| def _gemini_json(self, prompt): | |
| """One Gemini call returning a JSON object string.""" | |
| from google.genai import types | |
| response = self.client.models.generate_content( | |
| model=self.model, | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| response_mime_type="application/json", | |
| temperature=0.3, | |
| ), | |
| ) | |
| return response.text | |
| # ---- expert dialogue ---- | |
| def answer(self, question, material_text, history): | |
| """Answer a student question given material context and chat history.""" | |
| material = material_text[: self.cfg.max_context_chars] | |
| system = EXPERT_SYSTEM_PROMPT.format(material=material) | |
| if self.provider == "deepseek": | |
| return self._deepseek_answer(system, question, history) | |
| return self._gemini_answer(system, question, history) | |
| def _deepseek_answer(self, system, question, history): | |
| """Send question to DeepSeek and return the answer.""" | |
| messages = [{"role": "system", "content": system}] | |
| for turn in history[-8:]: | |
| role = "user" if turn["role"] == "student" else "assistant" | |
| messages.append({"role": role, "content": turn["text"]}) | |
| messages.append({"role": "user", "content": question}) | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=messages, | |
| temperature=0.7, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| def _gemini_answer(self, system, question, history): | |
| """Send question to Gemini and return the answer.""" | |
| from google.genai import types | |
| transcript = [] | |
| for turn in history[-8:]: | |
| speaker = "Student" if turn["role"] == "student" else "Expert" | |
| transcript.append(f"{speaker}: {turn['text']}") | |
| transcript.append(f"Student: {question}") | |
| conversation = "\n\n".join(transcript) | |
| response = self.client.models.generate_content( | |
| model=self.model, | |
| contents=conversation, | |
| config=types.GenerateContentConfig( | |
| system_instruction=system, | |
| temperature=0.7, | |
| ), | |
| ) | |
| return response.text.strip() | |