File size: 3,688 Bytes
da4758c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import hashlib
import json
import re
import types

import numpy as np
import pytest


class FakeEmbedder:
    """Deterministic bag-of-words hashing embedder, so similarity reflects shared words (no model download)."""

    def encode(self, texts, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False):
        out = np.zeros((len(texts), 256), dtype="float32")
        for i, text in enumerate(texts):
            for word in re.findall(r"[a-z]+", text.lower()):
                word = re.sub(r"(ing|s)$", "", word)  # crude stemming: drone/drones, monitor/monitoring
                out[i, int(hashlib.md5(word.encode()).hexdigest(), 16) % 256] += 1
        if normalize_embeddings:
            out /= np.maximum(np.linalg.norm(out, axis=1, keepdims=True), 1e-9)
        return out


WEB = {
    "solar": [
        {"title": "Agrivoltaics explained", "url": "https://a.example/agri",
         "content": "Agrivoltaics combines solar panels with crops on the same farmland. It improves land use and farm income. " * 10},
    ],
    "drone": [
        {"title": "Farm drones", "url": "https://b.example/drones",
         "content": "Drones monitor crops on farmland and help farmers detect plant disease early using imaging. " * 10},
        {"title": "Cooking pasta", "url": "https://c.example/pasta",
         "content": "Boil water, add salt, cook pasta for ten minutes and serve with sauce. " * 8},
    ],
}


def fake_searcher(question, max_results=4, search_depth="basic"):
    results = []
    for key, items in WEB.items():
        if key in question.lower():
            results += items
    return results or WEB["solar"]


class FakeLLM:
    """Scripted stand-in for Groq. Role is detected from the system prompt."""

    def __init__(self, bad_claim="The moon is made of cheese"):
        self.bad_claim = bad_claim
        self.calls = []

    def complete(self, messages, role="fast", temperature=0.0, json_mode=False):
        system, user = messages[0]["content"], messages[-1]["content"]
        self.calls.append(system.split(".")[0])
        if "Planner" in system:
            return json.dumps({"sub_questions": ["solar panels on farmland", "drone crop monitoring"]})
        if "fact-checker" in system:
            results = []
            for match in re.finditer(r"CLAIM (\d+): (.*)", user):
                verdict = "unsupported" if self.bad_claim.lower() in match.group(2).lower() else "supported"
                results.append({"id": int(match.group(1)), "verdict": verdict, "reason": "scripted"})
            return json.dumps({"results": results})
        if "Reviser" in system:
            report = user.split("A fact-checker flagged")[0].replace("REPORT:\n", "")
            return "\n".join(l for l in report.split("\n") if self.bad_claim.lower() not in l.lower())
        if "translator" in system:
            return "[UR] " + user.split("\n\n", 1)[1]
        raise AssertionError("unexpected prompt: " + system)

    def stream(self, messages, role="writer", temperature=0.3):
        self.calls.append("writer")
        text = (
            "## Key Findings\n"
            "Solar panels and crops can share the same farmland [1]. "
            "Drones help farmers detect plant disease early [2][9].\n"
            f"{self.bad_claim} [1].\n"
            "## Conclusion\n"
            "Both technologies improve how farmland is used [1, 2].\n"
        )
        for i in range(0, len(text), 25):
            yield text[i : i + 25]


@pytest.fixture
def embedder():
    return FakeEmbedder()


@pytest.fixture
def fake_llm():
    llm = FakeLLM()
    return types.SimpleNamespace(complete=llm.complete, stream=llm.stream, raw=llm)