tests/_mocks.py
Rama main · réplica de lectura del repositorio original.
"""Shared test doubles."""
class MockSubClient:
def __init__(self, model_name: str):
self.model_name = model_name
self.total_prompt_tokens = 0
self.total_completion_tokens = 0
self.total_requests = 0
class SmartMockLLM:
"""Mock DualLLMClient that returns matching answers depending on the prompt content."""
def __init__(self):
self.primary = MockSubClient("mock-primary")
self.harvest = MockSubClient("mock-harvest")
def _accounting(self, client_obj):
client_obj.total_requests += 1
client_obj.total_prompt_tokens += 200
client_obj.total_completion_tokens += 100
async def primary_complete(self, messages, max_tokens=None,
enable_thinking=None) -> str:
self._accounting(self.primary)
prompt = messages[0]["content"]
return self._answer_text(prompt)
async def primary_complete_json(self, messages, max_tokens=None) -> dict:
self._accounting(self.primary)
prompt = messages[0]["content"]
return self._answer_json(prompt)
async def harvest_complete(self, messages, max_tokens=None) -> str:
self._accounting(self.harvest)
prompt = messages[0]["content"]
return self._answer_text(prompt)
def get_usage_stats(self) -> dict:
return {
"primary": {
"model": self.primary.model_name,
"requests": self.primary.total_requests,
"prompt_tokens": self.primary.total_prompt_tokens,
"completion_tokens": self.primary.total_completion_tokens,
"total_tokens": (self.primary.total_prompt_tokens
+ self.primary.total_completion_tokens),
},
"harvest": {
"model": self.harvest.model_name,
"requests": self.harvest.total_requests,
"prompt_tokens": self.harvest.total_prompt_tokens,
"completion_tokens": self.harvest.total_completion_tokens,
"total_tokens": (self.harvest.total_prompt_tokens
+ self.harvest.total_completion_tokens),
},
}
# ─── Answer heuristics ───
def _answer_json(self, prompt: str) -> dict:
"""Return JSON structures matching the planning prompt."""
# PEER REVIEW Planning
if "Gutachten" in prompt and "sections" in prompt:
return {
"sections": ["Abstract", "Einleitung", "Methoden", "Ergebnisse"],
"criteria": ["Originalität", "Methodik", "Klarheit", "Beleglage"],
"paper_type": "empirisch",
"focus_note": "Fokus auf Methodik",
}
# PEER REVIEW recommendation (also JSON)
if "Wähle EINE Empfehlung" in prompt:
return {
"recommendation": "Major Revisions",
"summary": "Solide Grundlage, methodische Mängel.",
"must_address": ["Stichprobengröße", "Statistik"],
}
# PEER REVIEW criterion (comes as text, not JSON — dispatching)
# DECISION ANALYSIS planning
if "Entscheidungsanalyse" in prompt and "criteria" in prompt:
return {
"criteria": [
{"name": "Kosten", "weight": 30, "rationale": "..."},
{"name": "Nutzen", "weight": 40, "rationale": "..."},
{"name": "Risiko", "weight": 30, "rationale": "..."},
],
"blind_spots": ["Langzeit", "Stakeholder X"],
}
# DECISION evaluation
if "AUSSCHLIESSLICH dieses eine Option-Kriterium" in prompt:
return {
"score": 4,
"reasoning": "Mock-Begründung",
"uncertainty": "low",
"key_evidence": ["Punkt 1"],
}
# DECISION Aggregation
if "Aggregiere die Einzelbewertungen" in prompt:
return {
"options": [
{"name": "Option A", "weighted_score": 3.85,
"characterization": "Ausgewogen",
"best_criteria": ["Kosten"], "worst_criteria": ["Risiko"]},
],
"ranking": ["Option A", "Option B"],
}
# RESEARCH DESIGN Planning
if "Forschungsdesign" in prompt and "candidate_methods" in prompt:
return {
"candidate_methods": [
{"name": "Survey", "fit": "hoch", "rationale": "..."},
{"name": "Interviews", "fit": "mittel", "rationale": "..."},
],
"key_decisions": ["Sampling", "Operationalisierung"],
"discipline_traditions": ["Tradition A"],
}
# RESEARCH DESIGN Method comparison
if "fit_score" in prompt and "method" in prompt:
return {
"method": "Survey",
"fit_score": 4,
"strengths": ["Skalierbar", "Standardisiert"],
"weaknesses": ["Oberflächlich"],
"preconditions": ["Validiertes Instrument"],
"pitfalls": ["Antwort-Bias"],
"summary": "Passt gut",
}
# RESEARCH DESIGN Method recommendation
if "Methode wird empfohlen" in prompt:
return {
"primary_method": "Survey + qualitative Interviews",
"rationale": "Mixed-Methods passt zur Frage",
"alternative": "Reine Survey",
"alternative_rationale": "...",
"excluded": ["Experiment (nicht praktikabel)"],
}
# GRANT PROPOSAL Planning
if "Drittmittelantrag" in prompt and "work_packages" in prompt:
return {
"sections": ["Stand", "Ziele", "Methodik", "AP", "Verwertung"],
"main_goals": ["Ziel 1", "Ziel 2", "Ziel 3"],
"work_packages": [
{"id": "WP1", "title": "Vorbereitung", "months": "M1-M6"},
{"id": "WP2", "title": "Durchführung", "months": "M7-M24"},
{"id": "WP3", "title": "Auswertung", "months": "M25-M36"},
],
"critical_points": ["Methodik", "Budget"],
}
# GRANT PROPOSAL Consistency check
if "consistency_status" in prompt:
return {
"consistency_status": "alle Prüfungen bestanden",
"issues": [],
"recommendations": ["Vor Einreichung Korrektur lesen"],
}
# LIT REVIEW Planning
if "Literature Review" in prompt or "extraction_fields" in prompt:
return {
"operationalized_question": "Mock-Operationalisierung",
"sub_questions": ["Sub 1", "Sub 2"],
"extraction_fields": [
"Studientyp", "Stichprobe", "Methode", "Hauptergebnis",
],
"quality_criteria": [
"Methodische Transparenz", "Statistische Solidität",
],
"estimated_inclusion_rate": "70%",
}
# LIT REVIEW Screening
if "decision" in prompt and "include" in prompt:
return {
"decision": "include",
"reason": "Relevante Methodik",
"relevance_score": 4,
"matched_criteria": ["Methodik passt"],
"missing_criteria": [],
}
# LIT REVIEW Extraction
if "extracted" in prompt:
return {
"title": "Mock Paper Title",
"extracted": {
"Studientyp": "Querschnitt",
"Stichprobe": "n=200",
"Methode": "Survey",
"Hauptergebnis": "Effekt nachgewiesen",
},
"key_quote": "Ein zentraler Befund.",
"relation_to_question": "Beantwortet Sub-Frage 1",
}
# LIT REVIEW Quality
if "criteria_scores" in prompt:
return {
"overall_quality": "stark",
"criteria_scores": {
"Methodische Transparenz": {"score": 4, "rationale": "..."},
"Statistische Solidität": {"score": 4, "rationale": "..."},
},
"concerns": [],
}
# LIT REVIEW Clustering
if "clusters" in prompt:
return {
"clusters": [
{
"name": "Cluster A: Methodische Studien",
"theme": "Theme A",
"papers": ["P01", "P02"],
"consensus": "Konsens",
"tensions": "Keine",
},
],
}
# EXPLAINER Planning
if "Tiefenerklärung" in prompt or "core_concepts" in prompt:
return {
"audience_summary": "Mock audience",
"core_concepts": ["Konzept 1", "Konzept 2", "Konzept 3"],
"scope_note": "Scope",
}
return {}
def _answer_text(self, prompt: str) -> str:
"""Generic text-output mock."""
# Recognise synthesis prompts → longer text
if "Erstelle das finale" in prompt or "Erstelle den finalen" in prompt:
return (
"# Mock Finaler Bericht\n\n"
"## Zusammenfassung\nMock-Inhalt mit ausreichend Text.\n\n"
"## Hauptteil\nDetaillierte Mock-Erklärungen.\n\n"
"## Fazit\nMock-Schluss.\n"
)
# Standard output
return "Mock-Text-Output (mittlere Länge zur Demonstration)."