Study-with-ChampAI / agents /orchestrator.py
SolusOps's picture
feat: agents package
dc124db verified
Raw
History Blame Contribute Delete
5.43 kB
from __future__ import annotations
import uuid
from typing import List
from services.model_router import ModelRouter
from services.ingestion import load_file
from storage.local_db import DB
from storage.mastery import MasteryStore
from agents.document_agent import DocumentAgent, DocumentConcepts
from agents.quest_agent import QuestAgent
from agents.quiz_agent import QuizAgent
from agents.tutor_agent import TutorAgent
from agents.progress_agent import ProgressAgent
from agents.speech_agent import SpeechAgent
from agents.language_agent import LanguageAgent
from quiz.models import Quest, QuizSession, Question
from quiz.scoring import compute_grade
from config.prompts import QUEST_AGENT_SYSTEM
class LearningOrchestrator:
"""
Coordinates all specialist agents.
app.py talks only to this class — all agent wiring is here.
"""
def __init__(self, router: ModelRouter, db: DB, mastery_store: MasteryStore,
questions_per_quest: int = 3):
self.document = DocumentAgent(router)
self.quest = QuestAgent(router)
self.quiz = QuizAgent(router)
self.tutor = TutorAgent(router)
self.progress = ProgressAgent(mastery_store)
self.speech = SpeechAgent(router)
self.language = LanguageAgent(router)
self._router = router
self._db = db
self._mastery = mastery_store
self._questions_per_quest = questions_per_quest
def process_text(self, raw_text: str, language: str = "en") -> List[Quest]:
concepts = self.document.extract(raw_text)
return self._build_quests(concepts, language, source_text=raw_text)
def process_image(self, image_b64: str, language: str = "en") -> List[Quest]:
concepts = self.document.extract_from_image(image_b64)
return self._build_quests(concepts, language, source_text=concepts.ocr_text)
def process_voice(self, audio_bytes: bytes, language: str = "en") -> List[Quest]:
text = self.speech.transcribe(audio_bytes)
if not text:
raise ValueError("Speech transcription returned empty text.")
return self.process_text(text, language)
def process_file(self, file_path: str, language: str = "en") -> List[Quest]:
text, image_b64 = load_file(file_path)
if image_b64 and not text:
return self.process_image(image_b64, language)
if image_b64 and text:
return self.process_text(text, language) if text.strip() else self.process_image(image_b64, language)
return self.process_text(text, language)
def _build_quests(self, concepts: DocumentConcepts, language: str, source_text: str = "") -> List[Quest]:
quests = self.quest.generate({"topics": concepts.topics, "definitions": concepts.definitions,
"facts": concepts.facts, "formulae": concepts.formulae})
for quest in quests:
quest.questions = self.quiz.generate_for_quest(
quest, source_text=source_text,
questions_per_quest=self._questions_per_quest, language=language)
return quests
def generate_revision_quest(self, weak_topics: List[str], source_text: str = "") -> Quest:
"""
Nemotron generates a targeted revision quest for weak topics.
Adds to the user's quest queue — closes the learning loop.
"""
topics_str = ", ".join(weak_topics)
prompt = (f"Create a revision quest for these weak topics: {topics_str}\n"
f"Give it a dramatic RPG name like 'The Fallen Kingdom of {weak_topics[0]}'.\n"
f"Set boss_topic to the most fundamental topic that needs reviewing.\n"
f'Output: {{"quests":[{{"name":"string","topics":{weak_topics},"boss_topic":"string","difficulty":"medium"}}]}}')
try:
raw = self._router.reason(prompt, QUEST_AGENT_SYSTEM)
from services.json_parser import extract_json
data = extract_json(raw)
q_data = data["quests"][0]
quest = Quest(name=q_data["name"], topics=weak_topics,
boss_topic=q_data.get("boss_topic", weak_topics[-1]),
difficulty="medium")
except Exception:
quest = Quest(name=f"Revision: {weak_topics[0]}", topics=weak_topics,
boss_topic=weak_topics[-1], difficulty="medium")
quest.questions = self.quiz.generate_for_quest(quest, source_text=source_text)
return quest
def get_tutor_hint(self, question: Question, student_answer: str) -> str:
return self.tutor.hint(question=question.text, student_answer=student_answer,
correct_answer=question.correct_answer,
explanation=question.explanation,
source_excerpt=question.source_excerpt)
def translate_hint(self, text: str, target_lang: str) -> str:
return self.language.translate(text, target_lang)
def complete_quest(self, session: QuizSession) -> dict:
result = self.progress.update_from_session(session)
self._db.save_session(str(uuid.uuid4()), session.quest_name,
session.score, len(session.questions),
session.xp_earned,
compute_grade(session.score, len(session.questions)))
return result