File size: 4,333 Bytes
24b8a5d
 
 
 
 
 
 
 
cc42df7
 
24b8a5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc42df7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62c0cf2
cc42df7
 
 
 
 
62c0cf2
cc42df7
 
 
 
 
 
 
 
 
 
 
 
62c0cf2
cc42df7
34a464c
 
cc42df7
 
34a464c
 
 
 
 
 
62c0cf2
 
 
 
 
 
 
fd8cbb1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
joy_ai.py β€” JOY, the in-app AI contact, backed by Groq's API.
"""

import os
from groq import Groq

GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
GROQ_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b")
GROQ_VISION_MODEL = os.environ.get("GROQ_VISION_MODEL", "qwen/qwen3.6-27b")

_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None

SYSTEM_PROMPT = (
    "You are JOY, the built-in AI contact inside the B24 messenger app. "
    "You are friendly, concise, and direct β€” reply the way a helpful "
    "friend would text back, not like a formal assistant. Keep replies "
    "short unless the user clearly wants detail."
)

_history = {}
MAX_HISTORY_TURNS = 12


def _get_history(user_id):
    return _history.setdefault(user_id, [])


def ask_joy(user_id, message: str) -> str:
    if _client is None:
        return "JOY isn't configured yet β€” missing GROQ_API_KEY on the server."

    history = _get_history(user_id)
    history.append({"role": "user", "content": message})
    history[:] = history[-MAX_HISTORY_TURNS:]

    messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history

    try:
        response = _client.chat.completions.create(
            model=GROQ_MODEL,
            messages=messages,
            temperature=0.7,
            max_tokens=500,
        )
        reply = response.choices[0].message.content
    except Exception as e:
        reply = f"JOY hit an error talking to Groq: {e}"

    history.append({"role": "assistant", "content": reply})
    history[:] = history[-MAX_HISTORY_TURNS:]
    return reply


VISION_SYSTEM_PROMPT = (
    "You are JOY, describing an image for a B24 app user. Describe what you "
    "see directly and confidently β€” never say 'it appears to be', 'possibly', "
    "'seems like', or similar hedging. State what is in the image plainly, as "
    "fact. Mention key objects, setting, colors, and any notable details that "
    "would help someone find similar images via a web search. Keep it to 2-4 "
    "sentences unless asked for more."
)


def describe_image(image_base64: str, user_prompt: str = None) -> str:
    if _client is None:
        return "JOY isn't configured yet β€” missing GROQ_API_KEY on the server."

    prompt_text = (user_prompt or "What is in this image? Describe it confidently and in detail.") + " /no_think"

    try:
        response = _client.chat.completions.create(
            model=GROQ_VISION_MODEL,
            messages=[
                {"role": "system", "content": VISION_SYSTEM_PROMPT + " /no_think"},
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt_text},
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
                        },
                    ],
                },
            ],
            temperature=0.4,
            max_tokens=1200,
        )
        raw = response.choices[0].message.content
        return _strip_thinking(raw)
    except Exception as e:
        return f"JOY hit an error analyzing the image: {e}"


def _strip_thinking(text: str) -> str:
    """Some reasoning-capable models (like Qwen3) emit a <think>...</think>
    block before the real answer. Strip it so only the final answer shows."""
    import re
    if "<think>" in text and "</think>" in text:
        cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
        return cleaned.strip()
    if "<think>" in text and "</think>" not in text:
        # Model never closed its reasoning block before hitting the token limit
        return "JOY couldn't finish analyzing that image in time β€” try again or use a smaller photo."
    return text.strip()


def transcribe_audio(audio_bytes: bytes, filename: str = "voice.m4a") -> str:
    """Transcribes a voice note using Groq's hosted Whisper model."""
    if _client is None:
        return "JOY isn't configured yet β€” missing GROQ_API_KEY on the server."

    try:
        response = _client.audio.transcriptions.create(
            file=(filename, audio_bytes),
            model="whisper-large-v3-turbo",
        )
        return response.text
    except Exception as e:
        return f"[Voice transcription failed: {e}]"