Spaces:
Sleeping
Sleeping
File size: 6,427 Bytes
7ff3e94 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """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()
|