dialectica / scripts /expert.py
Kattine
Phase 8: provider toggle, layout fix, markdown rendering
7ff3e94
Raw
History Blame Contribute Delete
6.43 kB
"""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()