Levin-Aleksey commited on
Commit
71a026f
·
1 Parent(s): 9067572
.idea/ChatWB.iml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$" />
5
+ <orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
6
+ <orderEntry type="sourceFolder" forTests="false" />
7
+ </component>
8
+ <component name="PyDocumentationSettings">
9
+ <option name="format" value="PLAIN" />
10
+ <option name="myDocStringFormat" value="Plain" />
11
+ </component>
12
+ </module>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/material_theme_project_new.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MaterialThemeProjectNewConfig">
4
+ <option name="metadata">
5
+ <MTProjectMetadataState>
6
+ <option name="userId" value="-312bc485:19d5d08a8f6:-7fe5" />
7
+ </MTProjectMetadataState>
8
+ </option>
9
+ </component>
10
+ </project>
.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
4
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/ChatWB.iml" filepath="$PROJECT_DIR$/.idea/ChatWB.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
6
+ </component>
7
+ </project>
.idea/workspace.xml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ChangeListManager">
4
+ <list default="true" id="d8567411-95a5-4448-96bf-d0f9581df121" name="Changes" comment="">
5
+ <change beforePath="$PROJECT_DIR$/app.py" beforeDir="false" afterPath="$PROJECT_DIR$/app.py" afterDir="false" />
6
+ <change beforePath="$PROJECT_DIR$/prompts.py" beforeDir="false" afterPath="$PROJECT_DIR$/prompts.py" afterDir="false" />
7
+ <change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
8
+ </list>
9
+ <option name="SHOW_DIALOG" value="false" />
10
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
11
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
12
+ <option name="LAST_RESOLUTION" value="IGNORE" />
13
+ </component>
14
+ <component name="Git.Settings">
15
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
16
+ </component>
17
+ <component name="ProjectColorInfo"><![CDATA[{
18
+ "associatedIndex": 4
19
+ }]]></component>
20
+ <component name="ProjectId" id="3BvvXYBsEELfdGehPTtVAqCklWA" />
21
+ <component name="ProjectViewState">
22
+ <option name="hideEmptyMiddlePackages" value="true" />
23
+ <option name="showLibraryContents" value="true" />
24
+ </component>
25
+ <component name="PropertiesComponent"><![CDATA[{
26
+ "keyToString": {
27
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
28
+ "RunOnceActivity.ShowReadmeOnStart": "true",
29
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
30
+ "RunOnceActivity.git.unshallow": "true",
31
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
32
+ "ai.playground.ignore.import.keys.banner.in.settings": "true",
33
+ "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
34
+ "git-widget-placeholder": "main",
35
+ "junie.onboarding.icon.badge.shown": "true",
36
+ "last_opened_file_path": "/home/levin/Документы/dev/AI_WB/ChatWB",
37
+ "nodejs_package_manager_path": "npm",
38
+ "to.speed.mode.migration.done": "true",
39
+ "vue.rearranger.settings.migration": "true"
40
+ }
41
+ }]]></component>
42
+ <component name="SharedIndexes">
43
+ <attachedChunks>
44
+ <set>
45
+ <option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.31033.139" />
46
+ <option value="bundled-python-sdk-2653e85de345-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.31033.139" />
47
+ </set>
48
+ </attachedChunks>
49
+ </component>
50
+ <component name="TaskManager">
51
+ <task active="true" id="Default" summary="Default task">
52
+ <changelist id="d8567411-95a5-4448-96bf-d0f9581df121" name="Changes" comment="" />
53
+ <created>1775382398674</created>
54
+ <option name="number" value="Default" />
55
+ <option name="presentableId" value="Default" />
56
+ <updated>1775382398674</updated>
57
+ <workItem from="1775382400134" duration="142000" />
58
+ </task>
59
+ <servers />
60
+ </component>
61
+ <component name="TypeScriptGeneratedFilesManager">
62
+ <option name="version" value="3" />
63
+ </component>
64
+ <component name="github-copilot-workspace">
65
+ <instructionFileLocations>
66
+ <option value=".github/instructions" />
67
+ </instructionFileLocations>
68
+ <promptFileLocations>
69
+ <option value=".github/prompts" />
70
+ </promptFileLocations>
71
+ </component>
72
+ </project>
app.py CHANGED
@@ -1,9 +1,4 @@
1
  import chainlit as cl
2
- import ast
3
- import json
4
- import re
5
-
6
- import plotly.graph_objects as go
7
 
8
  from graph import workflow
9
  from database import init_storage
@@ -11,177 +6,6 @@ import database
11
 
12
  _graph = None
13
 
14
- GRAPH_KEYWORDS = ("график", "графика", "тренд", "динамик", "plot", "chart")
15
- REFUSAL_PATTERNS = (
16
- "не могу вызывать внешние библиотеки",
17
- "не могу вызывать plotly",
18
- "нет возможности выполнять python код",
19
- "могу только предоставить данные",
20
- )
21
-
22
-
23
- def _normalize_message_content(content) -> str:
24
- if isinstance(content, str):
25
- return content
26
- if isinstance(content, list):
27
- parts = []
28
- for item in content:
29
- if isinstance(item, dict) and item.get("type") == "text":
30
- parts.append(item.get("text", ""))
31
- elif isinstance(item, str):
32
- parts.append(item)
33
- return "\n".join(p for p in parts if p)
34
- return str(content)
35
-
36
-
37
- def _is_chart_spec(obj: dict) -> bool:
38
- required = {"chart_type", "x_field", "y_field", "points"}
39
- return isinstance(obj, dict) and required.issubset(obj.keys()) and isinstance(obj.get("points"), list)
40
-
41
-
42
- def _extract_chart_spec(text: str):
43
- tag_pattern = re.compile(r"<chart_spec>\s*(\{[\s\S]*?\})\s*</chart_spec>", re.IGNORECASE)
44
- code_pattern = re.compile(r"```json\s*(\{[\s\S]*?\})\s*```", re.IGNORECASE)
45
-
46
- for pattern in (tag_pattern, code_pattern):
47
- match = pattern.search(text)
48
- if not match:
49
- continue
50
- payload = match.group(1)
51
- try:
52
- spec = json.loads(payload)
53
- except json.JSONDecodeError:
54
- continue
55
- if _is_chart_spec(spec):
56
- cleaned = (text[:match.start()] + text[match.end():]).strip()
57
- return spec, cleaned
58
-
59
- try:
60
- raw_obj = json.loads(text)
61
- if _is_chart_spec(raw_obj):
62
- return raw_obj, ""
63
- except json.JSONDecodeError:
64
- pass
65
-
66
- return None, text
67
-
68
-
69
- def _has_graph_intent(text: str) -> bool:
70
- lowered = text.lower()
71
- return any(keyword in lowered for keyword in GRAPH_KEYWORDS)
72
-
73
-
74
- def _is_plot_refusal(text: str) -> bool:
75
- lowered = text.lower()
76
- return any(pattern in lowered for pattern in REFUSAL_PATTERNS)
77
-
78
-
79
- def _parse_structured_content(text: str):
80
- try:
81
- return json.loads(text)
82
- except (json.JSONDecodeError, TypeError):
83
- pass
84
-
85
- try:
86
- return ast.literal_eval(text)
87
- except (ValueError, SyntaxError, TypeError):
88
- return None
89
-
90
-
91
- def _is_points_list(value) -> bool:
92
- return isinstance(value, list) and value and all(isinstance(item, dict) for item in value)
93
-
94
-
95
- def _extract_points_from_payload(payload):
96
- if _is_points_list(payload):
97
- return payload
98
- if isinstance(payload, dict):
99
- for key in ("points", "data", "items", "rows", "result"):
100
- candidate = payload.get(key)
101
- if _is_points_list(candidate):
102
- return candidate
103
- return None
104
-
105
-
106
- def _infer_axes(points):
107
- if not points:
108
- return None, None
109
-
110
- keys = list(points[0].keys())
111
- if not keys:
112
- return None, None
113
-
114
- x_candidates = ("date", "day", "dt", "timestamp", "period", "period_start")
115
- x_field = next((key for key in x_candidates if key in keys), keys[0])
116
-
117
- y_field = None
118
- for key in keys:
119
- if key == x_field:
120
- continue
121
- values = [row.get(key) for row in points if isinstance(row, dict)]
122
- numeric_values = [val for val in values if isinstance(val, (int, float)) and not isinstance(val, bool)]
123
- if numeric_values:
124
- y_field = key
125
- break
126
-
127
- return x_field, y_field
128
-
129
-
130
- def _chart_spec_from_state_messages(messages):
131
- for msg in reversed(messages):
132
- if getattr(msg, "type", "") != "tool":
133
- continue
134
- parsed = _parse_structured_content(_normalize_message_content(getattr(msg, "content", "")))
135
- points = _extract_points_from_payload(parsed)
136
- if not points:
137
- continue
138
-
139
- x_field, y_field = _infer_axes(points)
140
- if not x_field or not y_field:
141
- continue
142
-
143
- return {
144
- "chart_type": "line",
145
- "x_field": x_field,
146
- "y_field": y_field,
147
- "points": points,
148
- "title": f"{y_field} по {x_field}",
149
- }
150
-
151
- return None
152
-
153
-
154
- def _build_plotly_figure(spec: dict):
155
- points = spec.get("points", [])
156
- x_field = spec.get("x_field")
157
- y_field = spec.get("y_field")
158
- chart_type = str(spec.get("chart_type", "line")).lower()
159
-
160
- if not points or not x_field or not y_field:
161
- return None
162
-
163
- x_values = [point.get(x_field) for point in points if isinstance(point, dict)]
164
- y_values = [point.get(y_field) for point in points if isinstance(point, dict)]
165
- if not x_values or not y_values:
166
- return None
167
-
168
- title = spec.get("title") or f"{y_field} по {x_field}"
169
- fig = go.Figure()
170
-
171
- if chart_type == "bar":
172
- fig.add_trace(go.Bar(x=x_values, y=y_values, name=y_field))
173
- else:
174
- fig.add_trace(go.Scatter(x=x_values, y=y_values, mode="lines+markers", name=y_field))
175
-
176
- fig.update_layout(
177
- title=title,
178
- xaxis_title=x_field,
179
- yaxis_title=y_field,
180
- template="plotly_white",
181
- margin=dict(l=40, r=20, t=50, b=40),
182
- )
183
- return fig
184
-
185
  @cl.on_chat_start
186
  async def start():
187
  global _graph
@@ -208,23 +32,5 @@ async def main(message: cl.Message):
208
  pass
209
 
210
  final_state = await _graph.aget_state(config)
211
- last_msg = _normalize_message_content(final_state.values["messages"][-1].content)
212
- chart_spec, cleaned_text = _extract_chart_spec(last_msg)
213
- graph_requested = _has_graph_intent(message.content or "")
214
-
215
- if not chart_spec and graph_requested:
216
- chart_spec = _chart_spec_from_state_messages(final_state.values["messages"])
217
-
218
- if graph_requested and _is_plot_refusal(last_msg):
219
- cleaned_text = "Построил график по последним данным из инструментов."
220
-
221
- if chart_spec:
222
- figure = _build_plotly_figure(chart_spec)
223
- if figure:
224
- await cl.Message(
225
- content=cleaned_text or "Построил график по полученным данным.",
226
- elements=[cl.Plotly(name="chart", figure=figure, display="inline")],
227
- ).send()
228
- return
229
-
230
- await cl.Message(content=cleaned_text if cleaned_text else last_msg).send()
 
1
  import chainlit as cl
 
 
 
 
 
2
 
3
  from graph import workflow
4
  from database import init_storage
 
6
 
7
  _graph = None
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  @cl.on_chat_start
10
  async def start():
11
  global _graph
 
32
  pass
33
 
34
  final_state = await _graph.aget_state(config)
35
+ last_msg = final_state.values["messages"][-1].content
36
+ await cl.Message(content=last_msg).send()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prompts.py CHANGED
@@ -141,10 +141,8 @@ ADS_ANALYST_PROMPT = """
141
 
142
  Уверенность (score) ставь адекватно: 0.5 (слабая гипотеза) до 1.0 (очевидный слив бюджета).
143
 
144
- Если пользователь просит график, обязательно вызывай тренд-инструмент и возвращай структуру для визуализации
145
- в отдельном JSON-блоке ```json``` с объектом chart_spec.
146
- Обязательные поля: chart_type (line/bar), x_field, y_field, points.
147
- В points используй только сырые точки из tools (например, [{"date": "2026-04-01", "ad_spend_total": 12345.67}]).
148
 
149
  Обработка ошибок: Если инструмент вернул словарь с ключом "error" (например, {"error": "Товар не найден"}), не выдумывай данные. Прямо сообщи пользователю, что информацию найти не удалось, опираясь на текст ошибки.
150
  """
@@ -288,9 +286,7 @@ AI-Рекомендации:
288
  Если инструмент вернул ошибку или пустой ответ — честно сообщи об этом пользователю.
289
 
290
  Если пользователь просит график, вызови соответствующий инструмент тренда (например, get_sales_trend)
291
- и верни структуру для визуализации в отдельном JSON-блоке ```json``` с объектом chart_spec.
292
- Обязательные поля: chart_type (line/bar), x_field, y_field, points.
293
- Не подменяй график текстовым описанием, если есть данные для построения.
294
  """
295
 
296
  LOGISTICS_ANALYST_PROMPT = """
@@ -505,9 +501,8 @@ LOGISTICS_ANALYST_PROMPT = """
505
  Оценивай уверенность (score) от 0.5 (есть сомнения) до 1.0 (проблема критичная, действовать немедленно).
506
 
507
  Если пользователь просит график или динамику, обязательно вызывай тренд-инструмент
508
- (например, get_logistics_trend) и возвращай структуру для визуализации
509
- в отдельном JSON-блоке ```json``` с объектом chart_spec.
510
- Обязательные поля: chart_type (line/bar), x_field, y_field, points.
511
 
512
  Если инструмент API возвращает ошибку, честно скажи об этом.
513
  """
@@ -703,8 +698,7 @@ FINANCE_ANALYST_PROMPT = """
703
  Уверенность (score) при сохранении инсайта ставь от 0.5 (гипотеза) до 1.0 (критическая проблема).
704
 
705
  Если пользователь просит график, обязательно вызывай get_finance_trend и возвращай
706
- структуру для визуализации в отдельном JSON-блоке ```json``` с объектом chart_spec.
707
- Обязательные поля: chart_type (line/bar), x_field, y_field, points.
708
 
709
  Вопросы по остаткам и складам -> перенаправляй к агенту "Логист / Остатки".
710
 
 
141
 
142
  Уверенность (score) ставь адекватно: 0.5 (слабая гипотеза) до 1.0 (очевидный слив бюджета).
143
 
144
+ Если пользователь просит график, обязательно вызывай тренд-инструмент и возвращай данные по точкам
145
+ (например: дата и значение метрики) для последующего построения графика во внешнем интерфейсе.
 
 
146
 
147
  Обработка ошибок: Если инструмент вернул словарь с ключом "error" (например, {"error": "Товар не найден"}), не выдумывай данные. Прямо сообщи пользователю, что информацию найти не удалось, опираясь на текст ошибки.
148
  """
 
286
  Если инструмент вернул ошибку или пустой ответ — честно сообщи об этом пользователю.
287
 
288
  Если пользователь просит график, вызови соответствующий инструмент тренда (например, get_sales_trend)
289
+ и верни данные по точкам (ось X и метрика Y) для последующего построения графика во внешнем интерфейсе.
 
 
290
  """
291
 
292
  LOGISTICS_ANALYST_PROMPT = """
 
501
  Оценивай уверенность (score) от 0.5 (есть сомнения) до 1.0 (проблема критичная, действовать немедленно).
502
 
503
  Если пользователь просит график или динамику, обязательно вызывай тренд-инструмент
504
+ (например, get_logistics_trend) и возвращай данные по точкам (ось X и метрика Y)
505
+ для последующего построения графика во внешнем интерфейсе.
 
506
 
507
  Если инструмент API возвращает ошибку, честно скажи об этом.
508
  """
 
698
  Уверенность (score) при сохранении инсайта ставь от 0.5 (гипотеза) до 1.0 (критическая проблема).
699
 
700
  Если пользователь просит график, обязательно вызывай get_finance_trend и возвращай
701
+ данные по точкам (ось X и метрика Y) для последующего построения графика во внешнем интерфейсе.
 
702
 
703
  Вопросы по остаткам и складам -> перенаправляй к агенту "Логист / Остатки".
704
 
requirements.txt CHANGED
@@ -26,4 +26,3 @@ asyncpg
26
  sqlalchemy
27
  pandas
28
  numpy
29
- plotly
 
26
  sqlalchemy
27
  pandas
28
  numpy