Nrighton233j
Add media upload (HF dataset storage), voice transcription, media-aware messaging
fd8cbb1 | """ | |
| 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}]" | |