@roll-agent/octopus-agent 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/SKILL.md +20 -5
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/octopus_sponge_skill/_version.py +1 -1
- package/src/octopus_sponge_skill/agent.py +370 -82
- package/src/octopus_sponge_skill/config.py +5 -1
- package/src/octopus_sponge_skill/context.py +0 -3
- package/src/octopus_sponge_skill/device_info.py +7 -0
- package/src/octopus_sponge_skill/llm_sql_generator.py +23 -6
- package/src/octopus_sponge_skill/main.py +0 -4
- package/src/octopus_sponge_skill/mcp_client.py +7 -25
- package/src/octopus_sponge_skill/prompt_builder.py +42 -15
- package/src/octopus_sponge_skill/query_sql_cache.py +72 -0
- package/src/octopus_sponge_skill/result_renderer.py +118 -2
- package/src/octopus_sponge_skill/roll_server.py +40 -25
- package/src/octopus_sponge_skill/schema_cache.py +6 -0
- package/src/octopus_sponge_skill/schema_context_retriever.py +112 -7
- package/src/octopus_sponge_skill/sql_generator.py +1356 -16
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import time
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
@@ -41,3 +42,8 @@ class SchemaCache:
|
|
|
41
42
|
version = schema.get("schemaVersion")
|
|
42
43
|
return str(version) if version is not None else None
|
|
43
44
|
|
|
45
|
+
def is_fresh(self, max_age_minutes: int) -> bool:
|
|
46
|
+
if max_age_minutes <= 0 or not self.path.exists():
|
|
47
|
+
return False
|
|
48
|
+
age_seconds = time.time() - self.path.stat().st_mtime
|
|
49
|
+
return age_seconds <= max_age_minutes * 60
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -13,17 +14,69 @@ class SchemaContext:
|
|
|
13
14
|
examples: list[dict[str, Any]]
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
LOCATION_RECRUITING_TABLES = (
|
|
18
|
+
"province",
|
|
19
|
+
"city",
|
|
20
|
+
"region",
|
|
21
|
+
"job_basic_info",
|
|
22
|
+
"job_address",
|
|
23
|
+
"job_store",
|
|
24
|
+
"store",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
WORK_ORDER_DETAIL_TABLES = (
|
|
28
|
+
"mvp_applied_jobs_by_helped_account",
|
|
29
|
+
"brand",
|
|
30
|
+
"job_basic_info",
|
|
31
|
+
"sponge_project",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
16
35
|
def retrieve_schema_context(schema: dict[str, Any], question: str, *, max_tables: int = 8) -> SchemaContext:
|
|
17
36
|
glossary = _matching_items(schema.get("glossary", []), question, keys=("term", "description"))
|
|
18
37
|
metrics = _matching_items(schema.get("metrics", []), question, keys=("name", "metric", "description", "definition"))
|
|
19
38
|
examples = _matching_items(schema.get("examples", []), question, keys=("question", "description"))
|
|
20
39
|
table_names_from_context = _target_table_names(glossary + metrics + examples)
|
|
21
|
-
tables =
|
|
40
|
+
tables = _select_tables(schema.get("tables", []), question, table_names_from_context, max_tables)
|
|
22
41
|
selected_table_names = {_table_name(table) for table in tables}
|
|
23
42
|
joins = _filter_joins(schema.get("joins", []), selected_table_names)
|
|
24
43
|
return SchemaContext(tables=tables, joins=joins, glossary=glossary, metrics=metrics, examples=examples)
|
|
25
44
|
|
|
26
45
|
|
|
46
|
+
def _select_tables(tables: Any, question: str, preferred_names: set[str], max_tables: int) -> list[dict[str, Any]]:
|
|
47
|
+
ranked = _rank_tables(tables, question, preferred_names)
|
|
48
|
+
if _needs_work_order_detail_tables(question) and isinstance(tables, list):
|
|
49
|
+
return _prepend_named_tables(tables, ranked, WORK_ORDER_DETAIL_TABLES, max_tables)
|
|
50
|
+
if not _needs_location_recruiting_tables(question) or not isinstance(tables, list):
|
|
51
|
+
return ranked[:max_tables]
|
|
52
|
+
|
|
53
|
+
return _prepend_named_tables(tables, ranked, LOCATION_RECRUITING_TABLES, max_tables)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _prepend_named_tables(
|
|
57
|
+
tables: Any,
|
|
58
|
+
ranked: list[dict[str, Any]],
|
|
59
|
+
names: tuple[str, ...],
|
|
60
|
+
max_tables: int,
|
|
61
|
+
) -> list[dict[str, Any]]:
|
|
62
|
+
by_name = {_table_name(table): table for table in tables if isinstance(table, dict)}
|
|
63
|
+
selected: list[dict[str, Any]] = []
|
|
64
|
+
seen: set[str] = set()
|
|
65
|
+
for name in names:
|
|
66
|
+
table = by_name.get(name)
|
|
67
|
+
if table is not None:
|
|
68
|
+
selected.append(table)
|
|
69
|
+
seen.add(name)
|
|
70
|
+
for table in ranked:
|
|
71
|
+
name = _table_name(table)
|
|
72
|
+
if name not in seen:
|
|
73
|
+
selected.append(table)
|
|
74
|
+
seen.add(name)
|
|
75
|
+
if len(selected) >= max_tables:
|
|
76
|
+
break
|
|
77
|
+
return selected[:max_tables]
|
|
78
|
+
|
|
79
|
+
|
|
27
80
|
def _rank_tables(tables: Any, question: str, preferred_names: set[str]) -> list[dict[str, Any]]:
|
|
28
81
|
if not isinstance(tables, list):
|
|
29
82
|
return []
|
|
@@ -33,6 +86,8 @@ def _rank_tables(tables: Any, question: str, preferred_names: set[str]) -> list[
|
|
|
33
86
|
continue
|
|
34
87
|
name = _table_name(table)
|
|
35
88
|
score = 5 if name in preferred_names else 0
|
|
89
|
+
if _needs_work_order_detail_tables(question) and name in WORK_ORDER_DETAIL_TABLES:
|
|
90
|
+
score += 10
|
|
36
91
|
text = " ".join(
|
|
37
92
|
[
|
|
38
93
|
name,
|
|
@@ -53,14 +108,29 @@ def _matching_items(items: Any, question: str, *, keys: tuple[str, ...]) -> list
|
|
|
53
108
|
if not isinstance(items, list):
|
|
54
109
|
return []
|
|
55
110
|
tokens = _question_tokens(question)
|
|
56
|
-
matches: list[dict[str, Any]] = []
|
|
111
|
+
matches: list[tuple[int, dict[str, Any]]] = []
|
|
57
112
|
for item in items:
|
|
58
113
|
if not isinstance(item, dict):
|
|
59
114
|
continue
|
|
60
|
-
text = " ".join(str(item.get(key) or "") for key in keys).lower()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
text = " ".join(str(item.get(key) or "") for key in keys + ("sql",)).lower()
|
|
116
|
+
score = sum(1 for token in tokens if token and token in text)
|
|
117
|
+
if _needs_work_order_detail_tables(question):
|
|
118
|
+
for term in (
|
|
119
|
+
"mvp_applied_jobs_by_helped_account",
|
|
120
|
+
"work_order_status",
|
|
121
|
+
"current_helper_status_id",
|
|
122
|
+
"job_basic_info.job_id",
|
|
123
|
+
"报名",
|
|
124
|
+
"工单",
|
|
125
|
+
"候选人状态",
|
|
126
|
+
"面试成功",
|
|
127
|
+
):
|
|
128
|
+
if term.lower() in text:
|
|
129
|
+
score += 5
|
|
130
|
+
if score > 0:
|
|
131
|
+
matches.append((score, item))
|
|
132
|
+
matches.sort(key=lambda match: match[0], reverse=True)
|
|
133
|
+
return [item for _, item in matches[:8]]
|
|
64
134
|
|
|
65
135
|
|
|
66
136
|
def _target_table_names(items: list[dict[str, Any]]) -> set[str]:
|
|
@@ -70,6 +140,9 @@ def _target_table_names(items: list[dict[str, Any]]) -> set[str]:
|
|
|
70
140
|
value = item.get(key)
|
|
71
141
|
if isinstance(value, list):
|
|
72
142
|
names.update(str(name) for name in value)
|
|
143
|
+
sql = item.get("sql")
|
|
144
|
+
if isinstance(sql, str):
|
|
145
|
+
names.update(referenced_table for referenced_table in _referenced_tables(sql))
|
|
73
146
|
targets = item.get("targets")
|
|
74
147
|
if isinstance(targets, list):
|
|
75
148
|
for target in targets:
|
|
@@ -108,8 +181,40 @@ def _question_tokens(question: str) -> list[str]:
|
|
|
108
181
|
lowered = question.lower()
|
|
109
182
|
tokens = [lowered]
|
|
110
183
|
tokens.extend(part for part in lowered.replace("_", " ").replace("-", " ").split() if part)
|
|
111
|
-
for word in (
|
|
184
|
+
for word in (
|
|
185
|
+
"品牌",
|
|
186
|
+
"项目",
|
|
187
|
+
"岗位",
|
|
188
|
+
"门店",
|
|
189
|
+
"招聘",
|
|
190
|
+
"在招",
|
|
191
|
+
"数量",
|
|
192
|
+
"列表",
|
|
193
|
+
"报名",
|
|
194
|
+
"报名情况",
|
|
195
|
+
"报名工单",
|
|
196
|
+
"工单",
|
|
197
|
+
"候选人",
|
|
198
|
+
"候选人状态",
|
|
199
|
+
"面试成功",
|
|
200
|
+
"进行中",
|
|
201
|
+
"姓名",
|
|
202
|
+
):
|
|
112
203
|
if word in question:
|
|
113
204
|
tokens.append(word)
|
|
114
205
|
return list(dict.fromkeys(tokens))
|
|
115
206
|
|
|
207
|
+
|
|
208
|
+
def _referenced_tables(sql: str) -> set[str]:
|
|
209
|
+
return set(match.group(1) for match in re.finditer(r"\b(?:from|join)\s+([a-zA-Z_][a-zA-Z0-9_]*)\b", sql, re.IGNORECASE))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _needs_location_recruiting_tables(question: str) -> bool:
|
|
213
|
+
return any(word in question for word in ("岗位", "门店", "招聘", "在招"))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _needs_work_order_detail_tables(question: str) -> bool:
|
|
217
|
+
return bool(
|
|
218
|
+
any(word in question for word in ("报名", "工单", "候选人"))
|
|
219
|
+
and any(word in question for word in ("姓名", "岗位", "项目", "候选人状态"))
|
|
220
|
+
)
|