Files
wiseclaw/backend/app/profile/store.py

277 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from datetime import datetime
from sqlalchemy.orm import Session
from app.db import AuditLogORM, TelegramUserProfileORM
from app.models import UserProfileRecord
SKIP_TOKENS = {"pas", "gec", "geç", "skip", "-"}
ONBOARDING_QUESTIONS: list[dict[str, str]] = [
{"field": "display_name", "prompt": "1/12 Sana nasıl hitap etmeliyim?"},
{"field": "bio", "prompt": "2/12 Kısaca kendini nasıl tanıtırsın?"},
{"field": "occupation", "prompt": "3/12 En çok hangi işle uğraşıyorsun?"},
{"field": "primary_use_cases", "prompt": "4/12 WiseClaw'ı en çok hangi işler için kullanacaksın? Virgülle ayırabilirsin."},
{"field": "answer_priorities", "prompt": "5/12 Cevaplarımda en çok neye önem veriyorsun? Örnek: hız, detay, yaratıcılık, teknik doğruluk."},
{"field": "tone_preference", "prompt": "6/12 Nasıl bir tonda konuşayım?"},
{"field": "response_length", "prompt": "7/12 Cevaplar kısa mı, orta mı, detaylı mı olsun?"},
{"field": "language_preference", "prompt": "8/12 Hangi dilde konuşalım?"},
{"field": "workflow_preference", "prompt": "9/12 İşlerde önce plan mı istersin, yoksa direkt aksiyon mu?"},
{"field": "interests", "prompt": "10/12 Özellikle ilgilendiğin konular veya hobilerin neler? Virgülle ayırabilirsin."},
{"field": "approval_preferences", "prompt": "11/12 Onay almadan yapmamamı istediğin şeyler neler? Virgülle ayırabilirsin."},
{"field": "avoid_preferences", "prompt": "12/12 Özellikle kaçınmamı istediğin bir üslup veya davranış var mı?"},
]
class UserProfileService:
def __init__(self, session: Session) -> None:
self.session = session
def get_profile(self, telegram_user_id: int) -> UserProfileRecord | None:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return None
return self._to_record(record)
def start_onboarding(self, telegram_user_id: int) -> str:
record = self._get_or_create_profile(telegram_user_id)
record.onboarding_completed = False
record.last_onboarding_step = 0
record.updated_at = datetime.utcnow()
self.session.add(
AuditLogORM(category="profile", message=f"profile:onboarding-started:{telegram_user_id}")
)
self.session.flush()
intro = (
"Ben WiseClaw. Seni daha iyi tanimak ve cevaplarimi sana gore ayarlamak icin 12 kisa soru soracagim.\n"
"Istersen herhangi bir soruya `pas` diyerek gecebilirsin.\n\n"
)
return intro + ONBOARDING_QUESTIONS[0]["prompt"]
def reset_onboarding(self, telegram_user_id: int) -> str:
record = self._get_or_create_profile(telegram_user_id)
record.display_name = None
record.bio = None
record.occupation = None
record.primary_use_cases = "[]"
record.answer_priorities = "[]"
record.tone_preference = None
record.response_length = None
record.language_preference = None
record.workflow_preference = None
record.interests = "[]"
record.approval_preferences = "[]"
record.avoid_preferences = None
record.onboarding_completed = False
record.last_onboarding_step = 0
record.updated_at = datetime.utcnow()
self.session.add(
AuditLogORM(category="profile", message=f"profile:onboarding-reset:{telegram_user_id}")
)
self.session.flush()
return "Profil sifirlandi. /tanisalim yazarak tekrar baslayabiliriz."
def is_onboarding_active(self, telegram_user_id: int) -> bool:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return False
return not record.onboarding_completed and record.last_onboarding_step < len(ONBOARDING_QUESTIONS)
def answer_onboarding(self, telegram_user_id: int, text: str) -> tuple[str, bool]:
record = self._get_or_create_profile(telegram_user_id)
step = min(record.last_onboarding_step, len(ONBOARDING_QUESTIONS) - 1)
question = ONBOARDING_QUESTIONS[step]
self._apply_answer(record, question["field"], text)
record.last_onboarding_step = step + 1
record.updated_at = datetime.utcnow()
if record.last_onboarding_step >= len(ONBOARDING_QUESTIONS):
record.onboarding_completed = True
self.session.add(
AuditLogORM(category="profile", message=f"profile:onboarding-completed:{telegram_user_id}")
)
self.session.flush()
return self.render_completion_message(record), True
self.session.add(
AuditLogORM(
category="profile",
message=f"profile:onboarding-step:{telegram_user_id}:{record.last_onboarding_step}",
)
)
self.session.flush()
return ONBOARDING_QUESTIONS[record.last_onboarding_step]["prompt"], False
def render_profile_summary(self, telegram_user_id: int) -> str:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return "Henuz bir profilin yok. /tanisalim yazarak baslayabiliriz."
profile = self._to_record(record)
lines = [
"Profil ozetin:",
f"- Hitap: {profile.display_name or 'belirtilmedi'}",
f"- Kisa tanitim: {profile.bio or 'belirtilmedi'}",
f"- Ugras alani: {profile.occupation or 'belirtilmedi'}",
f"- Kullanim amaci: {', '.join(profile.primary_use_cases) if profile.primary_use_cases else 'belirtilmedi'}",
f"- Oncelikler: {', '.join(profile.answer_priorities) if profile.answer_priorities else 'belirtilmedi'}",
f"- Ton: {profile.tone_preference or 'belirtilmedi'}",
f"- Cevap uzunlugu: {profile.response_length or 'belirtilmedi'}",
f"- Dil: {profile.language_preference or 'belirtilmedi'}",
f"- Calisma bicimi: {profile.workflow_preference or 'belirtilmedi'}",
f"- Ilgi alanlari: {', '.join(profile.interests) if profile.interests else 'belirtilmedi'}",
f"- Onay beklentileri: {', '.join(profile.approval_preferences) if profile.approval_preferences else 'belirtilmedi'}",
f"- Kacinmami istedigin seyler: {profile.avoid_preferences or 'belirtilmedi'}",
]
if not profile.onboarding_completed:
lines.append(
f"- Durum: onboarding devam ediyor, sira {profile.last_onboarding_step + 1}/{len(ONBOARDING_QUESTIONS)}"
)
return "\n".join(lines)
def render_preferences_summary(self, telegram_user_id: int) -> str:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return "Henuz tercihlerin kayitli degil. /tanisalim ile baslayabiliriz."
profile = self._to_record(record)
return "\n".join(
[
"Tercihlerin:",
f"- Ton: {profile.tone_preference or 'belirtilmedi'}",
f"- Cevap uzunlugu: {profile.response_length or 'belirtilmedi'}",
f"- Dil: {profile.language_preference or 'belirtilmedi'}",
f"- Calisma bicimi: {profile.workflow_preference or 'belirtilmedi'}",
f"- Oncelikler: {', '.join(profile.answer_priorities) if profile.answer_priorities else 'belirtilmedi'}",
f"- Onay beklentileri: {', '.join(profile.approval_preferences) if profile.approval_preferences else 'belirtilmedi'}",
f"- Kacinmami istedigin seyler: {profile.avoid_preferences or 'belirtilmedi'}",
]
)
def build_prompt_profile(self, telegram_user_id: int) -> str:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return ""
profile = self._to_record(record)
instructions: list[str] = []
if profile.display_name:
instructions.append(f"Kullaniciya `{profile.display_name}` diye hitap edebilirsin.")
if profile.language_preference:
instructions.append(f"Varsayilan dili `{profile.language_preference}` olarak kullan.")
if profile.tone_preference:
instructions.append(f"Cevap tonunu su tercihe uydur: {profile.tone_preference}.")
if profile.response_length:
instructions.append(f"Varsayilan cevap uzunlugu tercihi: {profile.response_length}.")
if profile.workflow_preference:
instructions.append(f"Is yapis tarzinda su tercihe uy: {profile.workflow_preference}.")
if profile.answer_priorities:
instructions.append(
"Kullanici su niteliklere oncelik veriyor: " + ", ".join(profile.answer_priorities) + "."
)
if profile.primary_use_cases:
instructions.append(
"WiseClaw'i en cok su isler icin kullaniyor: " + ", ".join(profile.primary_use_cases) + "."
)
if profile.interests:
instructions.append(
"Gerekirse ornekleri su ilgi alanlarina yaklastir: " + ", ".join(profile.interests) + "."
)
if profile.approval_preferences:
instructions.append(
"Su konularda once onay bekle: " + ", ".join(profile.approval_preferences) + "."
)
if profile.avoid_preferences:
instructions.append(f"Su uslup veya davranislardan kacin: {profile.avoid_preferences}.")
return "\n".join(f"- {item}" for item in instructions)
def profile_memory_summary(self, telegram_user_id: int) -> str:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
return ""
profile = self._to_record(record)
parts = []
if profile.display_name:
parts.append(f"hitap={profile.display_name}")
if profile.language_preference:
parts.append(f"dil={profile.language_preference}")
if profile.tone_preference:
parts.append(f"ton={profile.tone_preference}")
if profile.response_length:
parts.append(f"uzunluk={profile.response_length}")
if profile.workflow_preference:
parts.append(f"calisma={profile.workflow_preference}")
if profile.primary_use_cases:
parts.append("amac=" + ",".join(profile.primary_use_cases[:3]))
return "profile_summary:" + "; ".join(parts)
def _get_or_create_profile(self, telegram_user_id: int) -> TelegramUserProfileORM:
record = self.session.get(TelegramUserProfileORM, telegram_user_id)
if record is None:
record = TelegramUserProfileORM(
telegram_user_id=telegram_user_id,
primary_use_cases="[]",
answer_priorities="[]",
interests="[]",
approval_preferences="[]",
onboarding_completed=False,
last_onboarding_step=0,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
self.session.add(record)
self.session.flush()
return record
def _apply_answer(self, record: TelegramUserProfileORM, field: str, answer: str) -> None:
cleaned = answer.strip()
if cleaned.lower() in SKIP_TOKENS:
return
if field in {"primary_use_cases", "answer_priorities", "interests", "approval_preferences"}:
setattr(record, field, json.dumps(self._split_list(cleaned), ensure_ascii=False))
return
setattr(record, field, cleaned)
def _split_list(self, value: str) -> list[str]:
parts = [item.strip() for item in value.replace("\n", ",").split(",")]
return [item for item in parts if item]
def _decode_list(self, value: str) -> list[str]:
try:
payload = json.loads(value)
except json.JSONDecodeError:
return []
if not isinstance(payload, list):
return []
return [str(item).strip() for item in payload if str(item).strip()]
def _to_record(self, record: TelegramUserProfileORM) -> UserProfileRecord:
return UserProfileRecord(
telegram_user_id=record.telegram_user_id,
display_name=record.display_name,
bio=record.bio,
occupation=record.occupation,
primary_use_cases=self._decode_list(record.primary_use_cases),
answer_priorities=self._decode_list(record.answer_priorities),
tone_preference=record.tone_preference,
response_length=record.response_length,
language_preference=record.language_preference,
workflow_preference=record.workflow_preference,
interests=self._decode_list(record.interests),
approval_preferences=self._decode_list(record.approval_preferences),
avoid_preferences=record.avoid_preferences,
onboarding_completed=record.onboarding_completed,
last_onboarding_step=record.last_onboarding_step,
)
def render_completion_message(self, record: TelegramUserProfileORM) -> str:
profile = self._to_record(record)
summary = [
"Seni tanidim ve tercihlerini kaydettim.",
f"- Hitap: {profile.display_name or 'belirtilmedi'}",
f"- Ton: {profile.tone_preference or 'belirtilmedi'}",
f"- Dil: {profile.language_preference or 'belirtilmedi'}",
f"- Cevap uzunlugu: {profile.response_length or 'belirtilmedi'}",
f"- Calisma bicimi: {profile.workflow_preference or 'belirtilmedi'}",
]
return "\n".join(summary)