Levin-Aleksey commited on
Commit
9067572
·
1 Parent(s): 5bb0553
Files changed (1) hide show
  1. app.py +102 -1
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import chainlit as cl
 
2
  import json
3
  import re
4
 
@@ -10,6 +11,14 @@ import database
10
 
11
  _graph = None
12
 
 
 
 
 
 
 
 
 
13
 
14
  def _normalize_message_content(content) -> str:
15
  if isinstance(content, str):
@@ -57,6 +66,91 @@ def _extract_chart_spec(text: str):
57
  return None, text
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def _build_plotly_figure(spec: dict):
61
  points = spec.get("points", [])
62
  x_field = spec.get("x_field")
@@ -116,6 +210,13 @@ async def main(message: cl.Message):
116
  final_state = await _graph.aget_state(config)
117
  last_msg = _normalize_message_content(final_state.values["messages"][-1].content)
118
  chart_spec, cleaned_text = _extract_chart_spec(last_msg)
 
 
 
 
 
 
 
119
 
120
  if chart_spec:
121
  figure = _build_plotly_figure(chart_spec)
@@ -126,4 +227,4 @@ async def main(message: cl.Message):
126
  ).send()
127
  return
128
 
129
- await cl.Message(content=last_msg).send()
 
1
  import chainlit as cl
2
+ import ast
3
  import json
4
  import re
5
 
 
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):
 
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")
 
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)
 
227
  ).send()
228
  return
229
 
230
+ await cl.Message(content=cleaned_text if cleaned_text else last_msg).send()