@roll-agent/octopus-agent 0.0.1

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 ADDED
@@ -0,0 +1,109 @@
1
+ # Octopus Sponge NL2SQL Agent
2
+
3
+ 本仓库实现 Octopus 对接 Sponge MCP Server 的第一版 MVP。
4
+
5
+ ## 能力
6
+
7
+ - Schema Cache(表结构缓存):本地保存 Sponge 返回的 schema。
8
+ - MCP Client(MCP 客户端):调用 `get_schema`、`get_allowed_context`、`validate_sql`、`execute_sql`。
9
+ - MCP Sampling(采样):由 roll-core 代调 LLM 生成和修正 SQL。
10
+ - SQL Guard(SQL 保护规则):只允许单条 `SELECT`,必须带 `LIMIT`。
11
+ - Audit Log(审计日志):记录 trace、session、SQL hash、执行摘要。
12
+
13
+ ## 本地运行
14
+
15
+ ```bash
16
+ python -m octopus_sponge_skill "查询我能看到的品牌列表" --user-id 456
17
+ ```
18
+
19
+ 运行参数由 Agent 内置;用户本地只需要配置 Sponge MCP Server 地址和 Token。
20
+
21
+ ## roll-core 接入
22
+
23
+ 本仓库支持纯 Python stdio MCP 子 Agent 接入 roll-core。
24
+
25
+ ### npm 包安装
26
+
27
+ 发布到 npm 后,可通过 `roll agent install` 安装:
28
+
29
+ ```bash
30
+ roll agent install @roll-agent/octopus-agent
31
+ roll run octopus-agent diagnostic_status --json
32
+ ```
33
+
34
+ 该 npm package(npm 包)直接包含 Python 源码;`roll agent install` 不会再从 PyPI 下载 `octopus-sponge-skill`。
35
+ 安装机器上的 `python3` 需要是 Python 3.11+。
36
+
37
+ ### 本地源码注册
38
+
39
+ ```bash
40
+ roll agent add .
41
+ roll run octopus-agent diagnostic_status --json
42
+ roll run octopus-agent refresh_schema --json \
43
+ --input-json '{"traceId":"trace_schema_10001","refreshReason":"manual"}'
44
+ roll run octopus-agent query_sponge --json \
45
+ --question "查询我能看到的品牌列表" \
46
+ --user-id 456
47
+ ```
48
+
49
+ `refresh_schema` 必填 `traceId`。Agent 会自动读取本地缓存里的 `schemaVersion` 并传给 Sponge。
50
+
51
+ 指定输出格式:
52
+
53
+ ```bash
54
+ roll run octopus-agent query_sponge --json \
55
+ --input-json '{"question":"查询我能看到的品牌列表","userId":456,"resultFormat":"markdown"}'
56
+ ```
57
+
58
+ 支持 `text`、`yaml`、`md`、`markdown`、`html`。不传时会从问题里自动识别。
59
+
60
+ 调试 SQL:
61
+
62
+ ```bash
63
+ roll run octopus-agent query_sponge --json \
64
+ --input-json '{"question":"查询我能看到的品牌列表","userId":456,"includeSql":true}'
65
+ ```
66
+
67
+ `includeSql=true` 时会返回 `generatedSql`、`normalizedSql`、`repairedSql` 和 `validation`。
68
+
69
+ `package.json` 中声明了 roll-core 启动方式:
70
+
71
+ ```json
72
+ {
73
+ "rollAgent": {
74
+ "runtime": {"ownership": "on-demand", "transport": "stdio"},
75
+ "start": {
76
+ "command": "python3",
77
+ "args": ["-m", "octopus_sponge_skill.roll_server"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ 环境变量可放到 `roll.config.yaml`:
84
+
85
+ ```yaml
86
+ agents:
87
+ env:
88
+ octopus-agent:
89
+ SPONGE_MCP_BASE_URL: "https://sponge-mcp.duliday.com"
90
+ SPONGE_MCP_ACCESS_TOKEN: "xxxx"
91
+ ```
92
+
93
+ `SPONGE_MCP_BASE_URL` 只填域名根地址,不要带 `/mcp/admin/health`;Agent 会调用 `/mcp/tools/...`。
94
+
95
+ LLM 配置读取 roll-core 现有 `llm` 配置;Agent 不读取模型 API Key。
96
+ `query_sponge` 会通过 MCP Sampling 请求 roll-core 代调模型。
97
+
98
+ ## 本地持久化
99
+
100
+ - `data/sponge-schema-cache.json`:schema 缓存。
101
+ - `logs/octopus-sponge-audit.log`:审计日志。
102
+
103
+ 不新建数据库,不保存真实项目 ID、品牌 ID、数据库账号密码。
104
+
105
+ ## 自测
106
+
107
+ ```bash
108
+ PYTHONPATH=src python -m unittest discover -s tests
109
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: octopus-agent
3
+ description: Octopus Sponge NL2SQL Agent,把自然语言问题转换为只读 SQL,并通过 Sponge MCP Server 校验和执行。
4
+ metadata:
5
+ roll-env-file: references/env.yaml
6
+ ---
7
+
8
+ # Octopus Sponge Agent
9
+
10
+ ## 职责
11
+
12
+ 本 Agent 只负责 Sponge 数据查询:
13
+
14
+ - 读取和刷新 Schema Cache(表结构缓存)。
15
+ - 基于用户问题和 schema context,通过 MCP Sampling(采样)请求 roll-core 代调 LLM 生成只读 SQL。
16
+ - 调用 Sponge MCP Server 做权限上下文、SQL 校验和 SQL 执行。
17
+ - 记录 Audit Log(审计日志)。
18
+
19
+ ## Tools
20
+
21
+ | Tool | 中文解释 | 用途 |
22
+ |-|-|-|
23
+ | `diagnostic_status` | 诊断状态 | 检查配置、缓存和日志路径 |
24
+ | `refresh_schema` | 刷新表结构 | 人工刷新本地 schema 缓存 |
25
+ | `query_sponge` | 查询 Sponge 数据 | 自然语言查询入口 |
26
+
27
+ `refresh_schema` 必填 `traceId`;Agent 会自动读取本地缓存里的 `schemaVersion` 并传给 Sponge。
28
+
29
+ `query_sponge` 支持 `resultFormat`:`text`、`yaml`、`md`、`markdown`、`html`。不传时按用户问题自动识别。
30
+ 调试时可传 `includeSql=true` 返回 `generatedSql`、`normalizedSql`、`repairedSql` 和 `validation`;默认不返回 SQL。
31
+
32
+ ## 上层调用规则
33
+
34
+ 1. 查询前如需排查环境,先调用 `diagnostic_status`。
35
+ 2. schema 过期或缺失时,调用 `refresh_schema`。
36
+ 3. 用户提出业务数据查询时,调用 `query_sponge`。
37
+ 4. 不要求上层提供真实项目 ID 或品牌 ID;权限由 Sponge MCP Server 处理。
38
+ 5. 不允许绕过本 Agent 直接连业务数据库。
39
+ 6. 运行参数由 Agent 内置;本地只通过环境变量配置 Sponge 地址和 Token。
40
+ 7. LLM 由 roll-core 通过 MCP Sampling 代调,本 Agent 不读取模型 API Key。
41
+
42
+ ## 持久化
43
+
44
+ - `data/sponge-schema-cache.json`:Schema Cache(表结构缓存)。
45
+ - `logs/octopus-sponge-audit.log`:Audit Log(审计日志)。
46
+
47
+ 不新建数据库,不保存真实项目 ID、品牌 ID、数据库账号密码。
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ _src_package = Path(__file__).resolve().parent.parent / "src" / "octopus_sponge_skill"
6
+ if _src_package.exists():
7
+ __path__.append(str(_src_package))
8
+
9
+ from ._version import __version__
10
+
11
+ __all__ = ["__version__"]
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@roll-agent/octopus-agent",
3
+ "version": "0.0.1",
4
+ "description": "Octopus Sponge NL2SQL Agent",
5
+ "license": "UNLICENSED",
6
+ "keywords": [
7
+ "roll-agent",
8
+ "mcp",
9
+ "nl2sql"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "octopus_sponge_skill",
16
+ "src",
17
+ "references",
18
+ "SKILL.md",
19
+ "pyproject.toml",
20
+ "!**/__pycache__/**",
21
+ "!**/*.pyc",
22
+ "!**/*.egg-info/**"
23
+ ],
24
+ "rollAgent": {
25
+ "runtime": {
26
+ "ownership": "on-demand",
27
+ "transport": "stdio"
28
+ },
29
+ "start": {
30
+ "command": "python3",
31
+ "args": [
32
+ "-m",
33
+ "octopus_sponge_skill.roll_server"
34
+ ]
35
+ }
36
+ }
37
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "octopus-skill"
3
+ version = "0.0.1"
4
+ description = "Octopus Sponge NL2SQL Agent"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.scripts]
10
+ octopus-sponge = "octopus_sponge_skill.main:main"
11
+
12
+ [build-system]
13
+ requires = ["setuptools>=68"]
14
+ build-backend = "setuptools.build_meta"
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
@@ -0,0 +1,8 @@
1
+ env:
2
+ SPONGE_MCP_BASE_URL:
3
+ required: true
4
+ purpose: Sponge MCP Server 地址
5
+ example: https://sponge-mcp.duliday.com
6
+ SPONGE_MCP_ACCESS_TOKEN:
7
+ required: true
8
+ purpose: Sponge MCP Server 固定访问 Token
@@ -0,0 +1,5 @@
1
+ """Octopus Sponge NL2SQL Agent."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ main()
4
+
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,288 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from .audit_logger import AuditLogger, sql_hash
8
+ from .config import AppConfig
9
+ from .context import UserContext, new_session_id, new_trace_id
10
+ from .llm_sql_generator import Sampler, SamplingSqlGenerator
11
+ from .mcp_client import SpongeMcpClient
12
+ from .result_renderer import render_result
13
+ from .schema_cache import SchemaCache
14
+ from .schema_context_retriever import retrieve_schema_context
15
+ from .sql_generator import RuleBasedSqlGenerator, enforce_sql_rules
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class QueryResult:
20
+ answer: str
21
+ generated_sql: str
22
+ normalized_sql: str
23
+ repaired_sql: str | None
24
+ validation: dict[str, Any]
25
+
26
+
27
+ @dataclass
28
+ class SpongeAgent:
29
+ config: AppConfig
30
+ mcp_client: SpongeMcpClient
31
+ schema_cache: SchemaCache
32
+ audit_logger: AuditLogger
33
+ _last_validation_error: dict[str, Any] = field(default_factory=dict, init=False)
34
+ _last_validation_result: dict[str, Any] = field(default_factory=dict, init=False)
35
+
36
+ @classmethod
37
+ def from_config(cls, config: AppConfig) -> "SpongeAgent":
38
+ return cls(
39
+ config=config,
40
+ mcp_client=SpongeMcpClient(
41
+ base_url=config.sponge_mcp.base_url,
42
+ access_token=config.sponge_mcp.access_token,
43
+ timeout_ms=config.sponge_mcp.timeout_ms,
44
+ ),
45
+ schema_cache=SchemaCache(config.schema.cache_path),
46
+ audit_logger=AuditLogger(config.audit.log_path, enabled=config.audit.enabled),
47
+ )
48
+
49
+ def answer(
50
+ self,
51
+ *,
52
+ question: str,
53
+ user_id: int,
54
+ session_id: str | None = None,
55
+ device_id: str | None = None,
56
+ sampler: Sampler | None = None,
57
+ result_format: str | None = None,
58
+ ) -> str:
59
+ return self.query(
60
+ question=question,
61
+ user_id=user_id,
62
+ session_id=session_id,
63
+ device_id=device_id,
64
+ sampler=sampler,
65
+ result_format=result_format,
66
+ ).answer
67
+
68
+ def query(
69
+ self,
70
+ *,
71
+ question: str,
72
+ user_id: int,
73
+ session_id: str | None = None,
74
+ device_id: str | None = None,
75
+ sampler: Sampler | None = None,
76
+ result_format: str | None = None,
77
+ ) -> QueryResult:
78
+ context = UserContext(
79
+ user_id=user_id,
80
+ session_id=session_id or new_session_id(),
81
+ device_id=device_id,
82
+ )
83
+ sql_question = _strip_result_format_instruction(question)
84
+ schema = self._ensure_schema()
85
+ allowed_context = self._get_allowed_context(context, sql_question)
86
+ scope_hints = allowed_context.get("scopeHints", {})
87
+ if sampler is not None:
88
+ schema_context = retrieve_schema_context(schema, sql_question)
89
+ generator = SamplingSqlGenerator(
90
+ sampler=sampler,
91
+ default_limit=self.config.sql.default_limit,
92
+ max_limit=self.config.sql.max_limit,
93
+ )
94
+ generated = generator.generate(
95
+ question=sql_question,
96
+ schema_context=schema_context,
97
+ scope_hints=scope_hints,
98
+ )
99
+ else:
100
+ schema_context = None
101
+ generator = RuleBasedSqlGenerator(
102
+ default_limit=self.config.sql.default_limit,
103
+ max_limit=self.config.sql.max_limit,
104
+ )
105
+ generated = generator.generate(
106
+ question=sql_question,
107
+ schema=schema,
108
+ scope_hints=scope_hints,
109
+ )
110
+ enforce_sql_rules(generated.sql, max_limit=self.config.sql.max_limit)
111
+ normalized_sql = self._validate_sql(context, question, generated.sql)
112
+ repaired_sql: str | None = None
113
+ if sampler is not None and normalized_sql is None and schema_context is not None:
114
+ repaired = generator.repair(
115
+ question=sql_question,
116
+ original_sql=generated.sql,
117
+ validation_error=self._last_validation_error,
118
+ schema_context=schema_context,
119
+ scope_hints=scope_hints,
120
+ )
121
+ repaired_sql = repaired.sql
122
+ enforce_sql_rules(repaired.sql, max_limit=self.config.sql.max_limit)
123
+ normalized_sql = self._validate_sql(context, question, repaired.sql, raise_on_error=True)
124
+ if normalized_sql is None:
125
+ raise RuntimeError("SQL validation failed")
126
+ result = self._execute_sql(context, question, normalized_sql)
127
+ return QueryResult(
128
+ answer=render_result(question, result, result_format),
129
+ generated_sql=generated.sql,
130
+ normalized_sql=normalized_sql,
131
+ repaired_sql=repaired_sql,
132
+ validation=self._last_validation_result,
133
+ )
134
+
135
+ def _ensure_schema(self) -> dict[str, Any]:
136
+ cached_schema = self.schema_cache.load()
137
+ cached_version = str(cached_schema.get("schemaVersion")) if cached_schema and cached_schema.get("schemaVersion") else None
138
+ trace_id = new_trace_id("trace_schema")
139
+ try:
140
+ schema_response = self.mcp_client.get_schema(
141
+ trace_id=trace_id,
142
+ schema_version=cached_version,
143
+ refresh_reason="startup",
144
+ )
145
+ except Exception:
146
+ if cached_schema is not None:
147
+ self.audit_logger.log(
148
+ {
149
+ "traceId": trace_id,
150
+ "tool": "get_schema",
151
+ "schemaVersion": cached_version,
152
+ "fallback": "cached_schema",
153
+ }
154
+ )
155
+ return cached_schema
156
+ raise
157
+
158
+ if schema_response.get("changed") is False and cached_schema is not None:
159
+ self.audit_logger.log(
160
+ {
161
+ "traceId": trace_id,
162
+ "tool": "get_schema",
163
+ "schemaVersion": cached_version,
164
+ "changed": False,
165
+ }
166
+ )
167
+ return cached_schema
168
+
169
+ if schema_response.get("changed") is False and cached_schema is None:
170
+ schema_response = self.mcp_client.get_schema(
171
+ trace_id=new_trace_id("trace_schema"),
172
+ schema_version=None,
173
+ refresh_reason="startup",
174
+ )
175
+
176
+ if "tables" in schema_response:
177
+ self.schema_cache.save(schema_response)
178
+ self.audit_logger.log(
179
+ {
180
+ "traceId": trace_id,
181
+ "tool": "get_schema",
182
+ "schemaVersion": schema_response.get("schemaVersion"),
183
+ "changed": schema_response.get("changed"),
184
+ }
185
+ )
186
+ return schema_response
187
+
188
+ if cached_schema is not None:
189
+ return cached_schema
190
+ raise RuntimeError("Sponge get_schema did not return tables and no local schema cache exists")
191
+
192
+ def _get_allowed_context(self, context: UserContext, question: str) -> dict[str, Any]:
193
+ trace_id = new_trace_id()
194
+ result = self.mcp_client.get_allowed_context(
195
+ user_id=context.user_id,
196
+ trace_id=trace_id,
197
+ session_id=context.session_id,
198
+ device_id=context.device_id,
199
+ question=question,
200
+ )
201
+ self.audit_logger.log(
202
+ {
203
+ "traceId": trace_id,
204
+ "sessionId": context.session_id,
205
+ "userId": context.user_id,
206
+ "question": question,
207
+ "tool": "get_allowed_context",
208
+ }
209
+ )
210
+ return result
211
+
212
+ def _validate_sql(
213
+ self,
214
+ context: UserContext,
215
+ question: str,
216
+ sql: str,
217
+ *,
218
+ raise_on_error: bool = False,
219
+ ) -> str | None:
220
+ trace_id = new_trace_id()
221
+ result = self.mcp_client.validate_sql(
222
+ user_id=context.user_id,
223
+ trace_id=trace_id,
224
+ session_id=context.session_id,
225
+ sql=sql,
226
+ )
227
+ self._last_validation_result = result
228
+ self.audit_logger.log(
229
+ {
230
+ "traceId": trace_id,
231
+ "sessionId": context.session_id,
232
+ "userId": context.user_id,
233
+ "question": question,
234
+ "tool": "validate_sql",
235
+ "sqlHash": sql_hash(sql),
236
+ "validateResult": result,
237
+ "errorCode": result.get("error", {}).get("code") if isinstance(result.get("error"), dict) else None,
238
+ }
239
+ )
240
+ if not result.get("valid"):
241
+ error = result.get("error", {})
242
+ self._last_validation_error = error if isinstance(error, dict) else {"code": "UNKNOWN"}
243
+ code = error.get("code") if isinstance(error, dict) else "UNKNOWN"
244
+ if raise_on_error:
245
+ raise RuntimeError(f"SQL validation failed: {code}")
246
+ return None
247
+ return str(result.get("normalizedSql") or sql)
248
+
249
+ def _execute_sql(self, context: UserContext, question: str, sql: str) -> dict[str, Any]:
250
+ trace_id = new_trace_id()
251
+ result = self.mcp_client.execute_sql(
252
+ user_id=context.user_id,
253
+ trace_id=trace_id,
254
+ session_id=context.session_id,
255
+ sql=sql,
256
+ timeout_ms=self.config.sponge_mcp.timeout_ms,
257
+ )
258
+ self.audit_logger.log(
259
+ {
260
+ "traceId": trace_id,
261
+ "sessionId": context.session_id,
262
+ "userId": context.user_id,
263
+ "question": question,
264
+ "tool": "execute_sql",
265
+ "sqlHash": sql_hash(sql),
266
+ "executeResult": {
267
+ "success": result.get("success"),
268
+ "rowCount": result.get("rowCount"),
269
+ "elapsedMs": result.get("elapsedMs"),
270
+ },
271
+ "rowCount": result.get("rowCount"),
272
+ "elapsedMs": result.get("elapsedMs"),
273
+ }
274
+ )
275
+ return result
276
+
277
+
278
+ def _strip_result_format_instruction(question: str) -> str:
279
+ cleaned = question
280
+ patterns = [
281
+ r"[,,。;;]?\s*输出\s*(?:yaml|html|markdown|md)\s*(?:格式)?",
282
+ r"[,,。;;]?\s*用\s*表格\s*展示",
283
+ r"[,,。;;]?\s*以\s*(?:yaml|html|markdown|md)\s*(?:格式)?\s*输出",
284
+ r"[,,。;;]?\s*return\s+(?:yaml|html|markdown|md)",
285
+ ]
286
+ for pattern in patterns:
287
+ cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE)
288
+ return cleaned.strip() or question
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass
12
+ class AuditLogger:
13
+ path: Path
14
+ enabled: bool = True
15
+
16
+ def log(self, event: dict[str, Any]) -> None:
17
+ if not self.enabled:
18
+ return
19
+ self.path.parent.mkdir(parents=True, exist_ok=True)
20
+ record = {
21
+ "occurredAt": datetime.now(timezone.utc).isoformat(),
22
+ **event,
23
+ }
24
+ with self.path.open("a", encoding="utf-8") as file:
25
+ file.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
26
+
27
+
28
+ def sql_hash(sql: str) -> str:
29
+ return hashlib.sha256(sql.encode("utf-8")).hexdigest()
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ PACKAGE_ROOT = Path.cwd()
9
+ DEFAULT_SPONGE_MCP_BASE_URL = "https://sponge-mcp.duliday.com"
10
+ DEFAULT_SPONGE_MCP_ACCESS_TOKEN = ""
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class SpongeMcpConfig:
15
+ base_url: str
16
+ access_token: str
17
+ timeout_ms: int
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SchemaConfig:
22
+ cache_path: Path
23
+ refresh_on_start: bool
24
+ refresh_interval_minutes: int
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class SqlConfig:
29
+ max_repair_attempts: int
30
+ default_limit: int
31
+ max_limit: int
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AuditConfig:
36
+ enabled: bool
37
+ log_path: Path
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class AppConfig:
42
+ sponge_mcp: SpongeMcpConfig
43
+ schema: SchemaConfig
44
+ sql: SqlConfig
45
+ audit: AuditConfig
46
+
47
+
48
+ def load_config() -> AppConfig:
49
+ return AppConfig(
50
+ sponge_mcp=SpongeMcpConfig(
51
+ base_url=os.getenv("SPONGE_MCP_BASE_URL", DEFAULT_SPONGE_MCP_BASE_URL).rstrip("/"),
52
+ access_token=os.getenv("SPONGE_MCP_ACCESS_TOKEN", DEFAULT_SPONGE_MCP_ACCESS_TOKEN),
53
+ timeout_ms=30000,
54
+ ),
55
+ schema=SchemaConfig(
56
+ cache_path=PACKAGE_ROOT / "data/sponge-schema-cache.json",
57
+ refresh_on_start=True,
58
+ refresh_interval_minutes=1440,
59
+ ),
60
+ sql=SqlConfig(
61
+ max_repair_attempts=2,
62
+ default_limit=100,
63
+ max_limit=500,
64
+ ),
65
+ audit=AuditConfig(
66
+ enabled=True,
67
+ log_path=PACKAGE_ROOT / "logs/octopus-sponge-audit.log",
68
+ ),
69
+ )
70
+
71
+
72
+ def load_config_from_env() -> AppConfig:
73
+ return load_config()
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from uuid import uuid4
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class UserContext:
9
+ user_id: int
10
+ session_id: str
11
+ device_id: str | None = None
12
+
13
+
14
+ def new_trace_id(prefix: str = "trace") -> str:
15
+ return f"{prefix}_{uuid4().hex}"
16
+
17
+
18
+ def new_session_id() -> str:
19
+ return f"session_{uuid4().hex}"
20
+