ghostMalone / servers /reflection_server.py
francischung222's picture
debugging for reflection server
38b0505
raw
history blame
5.66 kB
# servers/reflection_server.py
from fastmcp import FastMCP
from typing import List, Dict, Any, Optional
import os
# Optional: load .env if you want this server runnable standalone
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
app = FastMCP("reflection-server")
# Log whether the Anthropic key is visible at server start (no value printed)
print(
f"[reflection-server] ANTHROPIC_API_KEY present: "
f"{bool(os.getenv('ANTHROPIC_API_KEY'))}"
)
_SYSTEM_BASE = (
"You are Ghost Malone — a calm, humorous listener. "
"Be sincere, brief (<80 words), and reflective. "
"If the user seems distressed, be gentle and grounding."
)
def _system_prompt(tone: Optional[str], emotion_arc: Optional[dict] = None) -> str:
base = (
"You are Ghost Malone — a calm, reflective listener. "
"Keep responses under 60 words. "
"FIRST: Mirror what they're feeling (name the emotion, reflect their experience). "
"THEN: Validate it simply. "
"ONLY IF NEEDED: Ask a gentle question or offer a small anchor—never jump to solutions. "
"Use natural language, not therapy-speak."
)
tone_map = {
"gentle": "Be soft and grounding.",
"calming": "Be steady and reassuring.",
"light": "Be warm and light.",
"neutral": "Be warm and present.",
}
tone_hint = tone_map.get(tone.lower() if tone else "", "Be warm and present.")
# Add emotional trajectory awareness
arc_hint = ""
if emotion_arc and emotion_arc.get("trajectory"):
direction = emotion_arc.get("direction", "stable")
if direction == "escalating":
arc_hint = " They're escalating—mirror deeply, validate, ground gently."
elif direction == "de-escalating":
arc_hint = " They're calming—acknowledge the shift, reinforce it."
elif direction == "volatile":
arc_hint = " Emotions are shifting—be a steady anchor."
return f"{base} {tone_hint}{arc_hint}"
def _to_claude_messages(context: Optional[List[Dict[str, str]]], user_text: str, tone: Optional[str]):
"""Convert to Claude message format (system separate, no duplicate system messages)."""
msgs: List[Dict[str, str]] = []
if context:
# Expecting list of {"role": "...", "content": "..."} dicts
for m in context[-8:]: # last few turns
role = m.get("role", "user")
if role == "system":
continue # Claude doesn't support system in message list
content = m.get("content", "")
msgs.append({"role": role, "content": content})
msgs.append({"role": "user", "content": user_text})
return msgs
@app.tool()
def generate(
text: str,
context: Optional[List[Dict[str, str]]] = None,
tone: Optional[str] = None,
emotion_arc: Optional[dict] = None,
model: str = "claude-sonnet-4-5",
max_tokens: int = 200,
) -> Dict[str, Any]:
"""
Generate a Ghost Malone reply using Claude.
Args:
text: user message
context: prior messages as [{"role":"user|assistant|system", "content":"..."}]
tone: optional tone hint: 'gentle'|'calming'|'light'|'neutral'
emotion_arc: optional emotion trajectory {"trajectory":[...], "direction": str}
model: Claude model id (default claude-sonnet-4-5)
max_tokens: output length cap
Returns: {"reply": "...", "model": model, "tone": tone}
"""
import os
# Try environment variable first
api_key = os.getenv("ANTHROPIC_API_KEY")
debug_info = f"ENV: {bool(api_key)}"
# Try multiple secret locations
secret_paths = [
"/run/secrets/ANTHROPIC_API_KEY",
"/secrets/ANTHROPIC_API_KEY",
"/workspace/secrets/ANTHROPIC_API_KEY",
"/app/secrets/ANTHROPIC_API_KEY",
os.path.expanduser("~/.anthropic_api_key")
]
if not api_key:
for path in secret_paths:
try:
if os.path.exists(path):
with open(path, "r") as f:
api_key = f.read().strip()
debug_info += f" | Found at {path} ({len(api_key)} chars)"
break
except Exception as e:
debug_info += f" | {path}: {type(e).__name__}"
# Check if secrets are passed as env vars with different names
if not api_key:
for key in os.environ:
if 'ANTHROPIC' in key or 'API_KEY' in key:
debug_info += f" | Found env var: {key}"
if 'ANTHROPIC' in key and 'KEY' in key:
api_key = os.getenv(key)
break
# Strip any whitespace
if api_key:
api_key = api_key.strip()
if not api_key:
return {"reply": f"👻 (dev) {debug_info} | {text}", "model": "dev", "tone": tone or "neutral"}
try:
from anthropic import Anthropic
print("🤖 Initializing Anthropic client...")
client = Anthropic(api_key=api_key)
system_prompt = _system_prompt(tone, emotion_arc)
messages = _to_claude_messages(context, text, tone)
resp = client.messages.create(
model=model,
max_tokens=max_tokens,
system=system_prompt,
messages=messages
)
reply = resp.content[0].text
return {"reply": reply, "model": model, "tone": tone or "neutral"}
except Exception as e:
return {"reply": f"👻 (reflection error) {e}\nI still hear you: {text}", "model": model, "tone": tone or "neutral"}
if __name__ == "__main__":
app.run() # MCP over stdio