-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalyzer.py
More file actions
180 lines (149 loc) · 7.03 KB
/
Copy pathanalyzer.py
File metadata and controls
180 lines (149 loc) · 7.03 KB
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
Stage 2 — AI-powered review summarisation and first-message generation.
Supports two backends:
1. Anthropic API — set ANTHROPIC_API_KEY env var (requires VPN)
2. LM Studio / any OpenAI-compatible local server
— pass endpoint="http://localhost:1234/v1" and model="<model name>"
— or set LM_STUDIO_URL env var (model defaults to "local-model")
"""
import json
import logging
import os
from typing import Optional
import anthropic
import httpx
from models import BusinessCard, Review
logger = logging.getLogger(__name__)
_anthropic_client: Optional[anthropic.Anthropic] = None
# ---------------------------------------------------------------------------
# Prompt templates
# ---------------------------------------------------------------------------
_SYSTEM_PROMPT = """\
Ты аналитик отзывов для малого бизнеса в сфере услуг (маникюр, барбершоп, массаж, косметология).
Твоя задача — прочитать отзывы о компании и составить:
1. Краткое резюме (1–2 предложения): общее впечатление клиентов.
2. Жалобы на запись/коммуникацию: конкретные проблемы, если они есть в отзывах
(путаница со временем, не дозвониться, долго ждали, забыли про запись, писали в директ/вотсап и т.п.).
Если таких жалоб нет — верни пустую строку.
3. Первое холодное сообщение (не длиннее 3 предложений) от лица продавца Telegram-бота для онлайн-записи.
— Если есть жалобы на запись/коммуникацию: упомяни конкретную боль из отзывов.
— Если жалоб нет: используй нейтральный шаблон:
«Увидел(а) вас на 2ГИС — заметил(а), что онлайн-записи нет. Как сейчас записываются клиенты?»
Обращайся на «вы». Не упоминай конкурентов. Не используй слово «бот» в первом сообщении — пиши «онлайн-запись».
Формат ответа — строго JSON без лишних пояснений:
{
"review_summary": "...",
"booking_complaints": "...",
"first_message": "..."
}"""
def _build_user_prompt(card: BusinessCard, reviews: list[Review]) -> str:
parts = [
f"Компания: {card.name}",
f"Категория: {card.category}",
f"Город: {card.city}",
f"Рейтинг: {card.rating}",
"",
"Отзывы:",
]
if not reviews:
parts.append("(отзывов нет)")
else:
for i, r in enumerate(reviews, 1):
rating_str = f" [{r.rating}★]" if r.rating else ""
parts.append(f"{i}. {r.author}{rating_str}: {r.text.strip()}")
return "\n".join(parts)
def _parse_response(raw: str) -> tuple[str, str]:
if raw.startswith("```"):
raw = "\n".join(l for l in raw.splitlines() if not l.startswith("```")).strip()
data = json.loads(raw)
return str(data.get("review_summary", "")).strip(), str(data.get("first_message", "")).strip()
def _neutral_first_message(card: BusinessCard) -> str:
return (
"Увидел(а) вас на 2ГИС — заметил(а), что онлайн-записи нет. "
"Как сейчас записываются клиенты?"
)
# ---------------------------------------------------------------------------
# Backends
# ---------------------------------------------------------------------------
def _call_anthropic(user_prompt: str) -> tuple[str, str]:
global _anthropic_client
if _anthropic_client is None:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise EnvironmentError("ANTHROPIC_API_KEY не задан")
_anthropic_client = anthropic.Anthropic(api_key=api_key)
resp = _anthropic_client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_prompt}],
)
return _parse_response(resp.content[0].text.strip())
def _call_local(endpoint: str, model: str, user_prompt: str) -> tuple[str, str]:
"""Call any OpenAI-compatible server (LM Studio, Ollama, etc.)."""
url = endpoint.rstrip("/") + "/chat/completions"
payload = {
"model": model,
"messages": [
{"role": "system", "content": _SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
"max_tokens": 1024,
"temperature": 0.3,
}
r = httpx.post(url, json=payload, timeout=120)
r.raise_for_status()
raw = r.json()["choices"][0]["message"]["content"].strip()
return _parse_response(raw)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def analyse(
card: BusinessCard,
reviews: list[Review],
*,
local_endpoint: str = "",
local_model: str = "",
) -> tuple[str, str]:
"""
Returns (review_summary, first_message).
If local_endpoint is set → uses LM Studio / OpenAI-compatible server.
Otherwise falls back to env vars:
LM_STUDIO_URL → local server
ANTHROPIC_API_KEY → Anthropic cloud
"""
user_prompt = _build_user_prompt(card, reviews)
# resolve backend
endpoint = local_endpoint or os.environ.get("LM_STUDIO_URL", "")
model = local_model or os.environ.get("LM_STUDIO_MODEL", "local-model")
try:
if endpoint:
return _call_local(endpoint, model, user_prompt)
else:
return _call_anthropic(user_prompt)
except json.JSONDecodeError as e:
logger.warning("analyse(%s): JSON parse error: %s", card.name, e)
return "", _neutral_first_message(card)
except (anthropic.APIError, httpx.HTTPError) as e:
logger.warning("analyse(%s): API error: %s", card.name, e)
return "", _neutral_first_message(card)
def analyse_and_script(
card: BusinessCard,
reviews: list[Review],
*,
local_endpoint: str = "",
local_model: str = "",
) -> BusinessCard:
"""
Runs analyse() + generate_dialog() and fills card fields in-place.
Returns the same card for chaining.
"""
from dialog_gen import generate_dialog # local import — avoids circular dep
summary, first_msg = analyse(
card, reviews, local_endpoint=local_endpoint, local_model=local_model
)
card.review_summary = summary
card.first_message = first_msg
endpoint = local_endpoint or os.environ.get("LM_STUDIO_URL", "")
card.script_dialog = generate_dialog(card, endpoint=endpoint, model=local_model or os.environ.get("LM_STUDIO_MODEL", "local-model"))
return card