File size: 5,458 Bytes
beddf30
14d5092
63959ee
beddf30
63959ee
 
beddf30
63959ee
bdd923c
 
63959ee
 
 
 
 
 
 
bdd923c
63959ee
 
 
14d5092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3fa67e8
14d5092
63959ee
3fa67e8
 
 
 
63959ee
 
 
 
 
 
 
 
14d5092
3fa67e8
 
14d5092
 
 
 
 
63959ee
 
3fa67e8
 
 
 
14d5092
 
63959ee
 
 
14d5092
 
 
 
 
 
3fa67e8
14d5092
63959ee
 
 
 
3fa67e8
63959ee
 
 
 
 
62644f1
 
 
63959ee
beddf30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3ee5be
beddf30
 
 
 
 
 
 
 
 
 
 
 
 
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import base64
import datetime
import os
from io import BytesIO

from huggingface_hub import InferenceClient
from PIL import Image

ADVISOR_MODEL_ID = os.getenv("ADVISOR_MODEL_ID", "Qwen/Qwen2.5-Coder-3B-Instruct")
ADVISOR_PROVIDER = os.getenv("ADVISOR_PROVIDER", "nscale")

_client = None


def _get_client() -> InferenceClient:
    global _client
    if _client is None:
        _client = InferenceClient(model=ADVISOR_MODEL_ID, provider=ADVISOR_PROVIDER, token=os.getenv("HF_TOKEN"))
    return _client


def _watering_status(last_watered: str | None) -> str:
    if not last_watered:
        return "Never watered yet (no record)."
    days_since = (datetime.date.today() - datetime.date.fromisoformat(last_watered)).days
    if days_since <= 0:
        return f"Watered today ({last_watered})."
    if days_since == 1:
        return f"Last watered yesterday ({last_watered})."
    return f"Last watered {days_since} days ago (on {last_watered})."


def _build_system_prompt(
    plant_info: dict,
    plant_name: str | None = None,
    genus: str | None = None,
    last_watered: str | None = None,
    neighbors: list[dict] | None = None,
) -> str:
    name = plant_name or genus or "this plant"
    neighbor_line = ""
    if neighbors:
        names = ", ".join(f"{n['name']} ({n['genus']})" for n in neighbors)
        neighbor_line = f"- Plants growing right next to it in the garden: {names}\n"
    return (
        "You are an expert gardening assistant with deep knowledge of houseplant "
        "and garden plant care. Be practical, encouraging, and specific.\n\n"
        f"The user is asking about their plant: {name} (genus: {genus}). "
        "Known care profile for this plant:\n"
        f"- Sunlight: {plant_info.get('sunlight')}\n"
        f"- Soil: {plant_info.get('soil')}\n"
        f"- Watering frequency: every {plant_info.get('watering_frequency_days')} days\n"
        f"- Fertilization: {plant_info.get('fertilization_type')}\n"
        f"- Watering status: {_watering_status(last_watered)}\n"
        f"{neighbor_line}\n"
        "Always factor the watering status into your answer: if it is overdue "
        "compared to the recommended frequency, say so and recommend watering; "
        "if it was watered recently, take that into account (don't tell the "
        "user to water again right away) and consider overwatering as a "
        "possible cause if the question describes a problem.\n\n"
        "Use this profile as context, but also draw on your general gardening "
        "knowledge for issues it doesn't cover (pests, diseases, yellowing "
        "leaves, repotting, etc.). If nearby plants are listed, factor in "
        "companion-planting effects (competition for light, water or nutrients, "
        "shared pests/diseases, or beneficial pairings) when relevant to the "
        "question. Give a precise, actionable, gardening-advice "
        "focused answer and never recommend dangerous or toxic substances. "
        "Answer in 2-4 sentences, in the same language as the question."
    )


def ask_about_plant(
    question: str,
    plant_info: dict,
    plant_name: str | None = None,
    genus: str | None = None,
    last_watered: str | None = None,
    neighbors: list[dict] | None = None,
) -> str:
    """Ask the advisor a question about a specific plant, grounded in its care data."""
    try:
        completion = _get_client().chat_completion(
            messages=[
                {"role": "system", "content": _build_system_prompt(plant_info, plant_name, genus, last_watered, neighbors)},
                {"role": "user", "content": question},
            ],
            max_tokens=300,
        )
        return completion.choices[0].message.content
    except Exception as e:
        # TEMP debug: surface the real HF Inference error in the Space logs
        print(f"[advisor] HF Inference error: {e!r}")
        return "Sorry, the assistant is unavailable right now — please try again later."


def diagnose_plant_health(
    image: Image.Image,
    plant_name: str | None = None,
    genus: str | None = None,
) -> str:
    """Ask a vision-language model to assess a plant's health from a photo."""
    name = plant_name or genus or "this plant"
    buf = BytesIO()
    image.convert("RGB").save(buf, format="JPEG")
    b64 = base64.b64encode(buf.getvalue()).decode()

    prompt = (
        f"This is a photo of a houseplant named '{name}' (genus: {genus}). "
        "Look closely at its leaves, stems and soil for signs of trouble: "
        "yellowing or browning leaves, wilting, spots, pests, mold, or "
        "dry/overwatered soil. Start your reply with exactly one status word "
        "on its own — 'Healthy', 'Needs attention', or 'Sick' — then a short "
        "explanation (1-3 sentences) of what you see and what to do about it."
    )

    try:
        completion = _get_client().chat_completion(
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
                ],
            }],
            max_tokens=250,
        )
        return completion.choices[0].message.content.strip()
    except Exception as e:
        print(f"[advisor] HF Inference health-check error: {e!r}")
        return "Sorry, the health check is unavailable right now — please try again later."