Ctrl K
ComunidadGuías…

tests/_mocks.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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)."