feat: backend orkestrasyonunu ve arac entegrasyonlarini genislet

This commit is contained in:
2026-03-22 04:45:43 +03:00
parent d07bc365f5
commit 5f4c19a18d
25 changed files with 3750 additions and 82 deletions

View File

@@ -0,0 +1,276 @@
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)