init
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Scripts 模块
|
||||
|
||||
## 目录说明
|
||||
|
||||
`scripts` 提供本地调试与数据同步脚本。
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `console_chat.py`:命令行多轮问答调试
|
||||
- `demo_chat.py`:示例交互脚本
|
||||
- `sync_ragflow_templates.py`:同步表检索模板到 RAGFlow
|
||||
- `sync_sql_gen_prompts.py`:同步 SQL 提示词到 RAGFlow
|
||||
|
||||
## 常用命令
|
||||
|
||||
```powershell
|
||||
python scripts\console_chat.py --skip-sr-api --show-sql
|
||||
python scripts\sync_ragflow_templates.py
|
||||
python scripts\sync_sql_gen_prompts.py
|
||||
```
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env python3
|
||||
"""控制台问数脚本:支持交互式多轮问答和单次执行。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional, Sequence
|
||||
|
||||
# 允许直接使用 `python scripts/console_chat.py` 运行
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from agent.agents.conversation import ConversationAgent
|
||||
from config import Config
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="在控制台中直接输入自然语言问题,调用 ConversationAgent 进行问数。"
|
||||
)
|
||||
parser.add_argument("--model-section", default=None, help="可选:指定 config.ini 中的模型配置段")
|
||||
parser.add_argument("--conversation-id", default=None, help="可选:会话 ID,仅用于展示和上下文标识")
|
||||
parser.add_argument("--user", default="console-user", help="可选:用户标识")
|
||||
parser.add_argument("--query", "-q", default=None, help="单次执行模式:直接执行一条问题后退出")
|
||||
parser.add_argument("--skip-sr-api", action="store_true", help="仅生成 SQL,不执行 SR API")
|
||||
parser.add_argument("--show-sql", action="store_true", help="额外打印生成的 SQL")
|
||||
parser.add_argument("--show-context", action="store_true", help="额外打印完整上下文 JSON")
|
||||
parser.add_argument("--show-plan", action="store_true", help="额外打印 SQL 规划 JSON")
|
||||
parser.add_argument("--no-banner", action="store_true", help="不显示启动横幅")
|
||||
return parser
|
||||
|
||||
|
||||
def print_banner(model_section: Optional[str], conversation_id: str, skip_sr_api: bool) -> None:
|
||||
print("=" * 72)
|
||||
print("APBO Console Chat")
|
||||
print(f"Model Section : {model_section or 'default'}")
|
||||
print(f"Conversation : {conversation_id}")
|
||||
print(f"Execution : {'SQL only' if skip_sr_api else 'SQL + SR API'}")
|
||||
print("Commands : /quit /exit /sql /context /plan /exec")
|
||||
print("=" * 72)
|
||||
|
||||
|
||||
def _safe_json(data: Any) -> str:
|
||||
try:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2, default=str)
|
||||
except Exception:
|
||||
return str(data)
|
||||
|
||||
|
||||
def _looks_like_json(text: str) -> bool:
|
||||
stripped = (text or "").strip()
|
||||
return stripped.startswith("{") or stripped.startswith("[")
|
||||
|
||||
|
||||
def _try_json_loads(value: Any) -> Any:
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str) and _looks_like_json(value):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def parse_sr_api_result(raw_result: Any) -> Any:
|
||||
"""解析 SR API 返回值,兼容外层 envelope 和内层 text JSON。"""
|
||||
parsed = _try_json_loads(raw_result)
|
||||
if isinstance(parsed, dict) and "text" in parsed:
|
||||
text_payload = _try_json_loads(parsed.get("text"))
|
||||
parsed = {**parsed, "text": text_payload}
|
||||
return parsed
|
||||
|
||||
|
||||
def _find_table_candidate(value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
if "columns" in value and any(key in value for key in ("rows", "data", "values")):
|
||||
return value
|
||||
for key in ("data", "rows", "records", "record", "items", "list", "result", "text"):
|
||||
nested = value.get(key)
|
||||
if isinstance(nested, (list, dict)):
|
||||
found = _find_table_candidate(nested)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def extract_table_rows(parsed_result: Any) -> tuple[list[str], list[list[str]]]:
|
||||
"""从常见查询结果结构中提取表头和二维行数据。"""
|
||||
candidate = _find_table_candidate(parsed_result)
|
||||
if candidate is None:
|
||||
return [], []
|
||||
|
||||
if isinstance(candidate, dict) and isinstance(candidate.get("columns"), list):
|
||||
headers = [str(col) for col in candidate.get("columns") or []]
|
||||
raw_rows = candidate.get("rows") or candidate.get("data") or candidate.get("values") or []
|
||||
if raw_rows and all(isinstance(row, dict) for row in raw_rows):
|
||||
return headers or list(raw_rows[0].keys()), [
|
||||
[str((row or {}).get(header, "")) for header in (headers or list(raw_rows[0].keys()))]
|
||||
for row in raw_rows
|
||||
]
|
||||
return headers, [[str(cell) for cell in row] for row in raw_rows if isinstance(row, (list, tuple))]
|
||||
|
||||
if isinstance(candidate, list) and candidate:
|
||||
if all(isinstance(row, dict) for row in candidate):
|
||||
headers: list[str] = []
|
||||
for row in candidate:
|
||||
for key in row.keys():
|
||||
if key not in headers:
|
||||
headers.append(str(key))
|
||||
return headers, [[str((row or {}).get(header, "")) for header in headers] for row in candidate]
|
||||
if all(isinstance(row, (list, tuple)) for row in candidate):
|
||||
width = max(len(row) for row in candidate)
|
||||
headers = [f"col_{idx + 1}" for idx in range(width)]
|
||||
return headers, [[str(row[idx]) if idx < len(row) else "" for idx in range(width)] for row in candidate]
|
||||
|
||||
return [], []
|
||||
|
||||
|
||||
def render_text_table(headers: Sequence[str], rows: Sequence[Sequence[str]], *, max_width: int = 28, max_rows: int = 20) -> str:
|
||||
"""将二维数据渲染成纯文本表格。"""
|
||||
if not headers or not rows:
|
||||
return "<empty table>"
|
||||
|
||||
def clip(value: Any) -> str:
|
||||
text = str(value).replace("\r", " ").replace("\n", " ")
|
||||
return text if len(text) <= max_width else text[: max_width - 3] + "..."
|
||||
|
||||
display_rows = list(rows[:max_rows])
|
||||
str_rows = [[clip(cell) for cell in row] for row in display_rows]
|
||||
clipped_headers = [clip(header) for header in headers]
|
||||
|
||||
widths = []
|
||||
for idx, header in enumerate(clipped_headers):
|
||||
col_values = [row[idx] if idx < len(row) else "" for row in str_rows]
|
||||
widths.append(max(len(header), *(len(value) for value in col_values)) if col_values else len(header))
|
||||
|
||||
def render_row(values: Sequence[str]) -> str:
|
||||
padded = []
|
||||
for idx, width in enumerate(widths):
|
||||
value = values[idx] if idx < len(values) else ""
|
||||
padded.append(value.ljust(width))
|
||||
return "| " + " | ".join(padded) + " |"
|
||||
|
||||
separator = "+-" + "-+-".join("-" * width for width in widths) + "-+"
|
||||
lines = [separator, render_row(clipped_headers), separator]
|
||||
lines.extend(render_row(row) for row in str_rows)
|
||||
lines.append(separator)
|
||||
if len(rows) > max_rows:
|
||||
lines.append(f"... showing first {max_rows} of {len(rows)} rows")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _format_sr_api_table(sr_api_result: Any) -> Optional[str]:
|
||||
parsed = parse_sr_api_result(sr_api_result)
|
||||
headers, rows = extract_table_rows(parsed)
|
||||
if not headers or not rows:
|
||||
return None
|
||||
|
||||
summary = f"Query Result: {len(rows)} row(s)"
|
||||
if isinstance(parsed, dict) and parsed.get("status_code") is not None:
|
||||
summary += f" | status={parsed.get('status_code')}"
|
||||
return summary + "\n" + render_text_table(headers, rows)
|
||||
|
||||
|
||||
def format_result(
|
||||
result: Dict[str, Any],
|
||||
*,
|
||||
show_sql: bool = False,
|
||||
show_context: bool = False,
|
||||
show_plan: bool = False,
|
||||
) -> str:
|
||||
context = (result or {}).get("context") or {}
|
||||
messages = (result or {}).get("messages") or []
|
||||
sr_api_result = context.get("sr_api_result")
|
||||
|
||||
answer = ""
|
||||
if messages:
|
||||
last = messages[-1]
|
||||
answer = getattr(last, "content", "") or str(last)
|
||||
if not answer:
|
||||
answer = str(sr_api_result or context.get("final_sql") or "<empty response>")
|
||||
|
||||
table_block = _format_sr_api_table(sr_api_result) if sr_api_result else None
|
||||
if table_block and (_looks_like_json(answer) or answer.strip().lower() in {"ok", "success"}):
|
||||
answer = "查询成功,结果已按二维表格展示如下。"
|
||||
|
||||
blocks = [f"Answer:\n{answer}"]
|
||||
|
||||
if table_block:
|
||||
blocks.append(table_block)
|
||||
|
||||
if show_sql and context.get("final_sql"):
|
||||
blocks.append(f"SQL:\n{context['final_sql']}")
|
||||
|
||||
if show_plan and context.get("sql_plan"):
|
||||
blocks.append(f"SQL Plan:\n{_safe_json(context['sql_plan'])}")
|
||||
|
||||
if show_context:
|
||||
blocks.append(f"Context:\n{_safe_json(context)}")
|
||||
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
def run_turn(
|
||||
agent: ConversationAgent,
|
||||
query: str,
|
||||
*,
|
||||
user: str,
|
||||
conversation_id: str,
|
||||
skip_sr_api: bool,
|
||||
show_sql: bool,
|
||||
show_context: bool,
|
||||
show_plan: bool,
|
||||
) -> Dict[str, Any]:
|
||||
result = agent.run(
|
||||
query,
|
||||
user=user,
|
||||
conversation_id=conversation_id,
|
||||
skip_sr_api=skip_sr_api,
|
||||
debug_node_trace=True,
|
||||
)
|
||||
print(format_result(result, show_sql=show_sql, show_context=show_context, show_plan=show_plan))
|
||||
return result
|
||||
|
||||
|
||||
def interactive_loop(args: argparse.Namespace) -> int:
|
||||
conversation_id = args.conversation_id or f"console_{uuid.uuid4().hex[:8]}"
|
||||
agent = ConversationAgent(model_section=args.model_section)
|
||||
|
||||
show_sql = bool(args.show_sql)
|
||||
show_context = bool(args.show_context)
|
||||
show_plan = bool(args.show_plan)
|
||||
skip_sr_api = bool(args.skip_sr_api)
|
||||
|
||||
if not args.no_banner:
|
||||
print_banner(args.model_section, conversation_id, skip_sr_api)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n问数> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nBye.")
|
||||
return 0
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
lowered = user_input.lower()
|
||||
if lowered in {"/quit", "/exit", "quit", "exit"}:
|
||||
print("Bye.")
|
||||
return 0
|
||||
if lowered == "/sql":
|
||||
show_sql = not show_sql
|
||||
print(f"show_sql = {show_sql}")
|
||||
continue
|
||||
if lowered == "/context":
|
||||
show_context = not show_context
|
||||
print(f"show_context = {show_context}")
|
||||
continue
|
||||
if lowered == "/plan":
|
||||
show_plan = not show_plan
|
||||
print(f"show_plan = {show_plan}")
|
||||
continue
|
||||
if lowered == "/exec":
|
||||
skip_sr_api = not skip_sr_api
|
||||
print(f"skip_sr_api = {skip_sr_api}")
|
||||
continue
|
||||
|
||||
try:
|
||||
run_turn(
|
||||
agent,
|
||||
user_input,
|
||||
user=args.user,
|
||||
conversation_id=conversation_id,
|
||||
skip_sr_api=skip_sr_api,
|
||||
show_sql=show_sql,
|
||||
show_context=show_context,
|
||||
show_plan=show_plan,
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"\n[ERROR] {exc}")
|
||||
traceback.print_exc()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def one_shot(args: argparse.Namespace) -> int:
|
||||
conversation_id = args.conversation_id or f"console_{uuid.uuid4().hex[:8]}"
|
||||
agent = ConversationAgent(model_section=args.model_section)
|
||||
try:
|
||||
run_turn(
|
||||
agent,
|
||||
args.query,
|
||||
user=args.user,
|
||||
conversation_id=conversation_id,
|
||||
skip_sr_api=bool(args.skip_sr_api),
|
||||
show_sql=bool(args.show_sql),
|
||||
show_context=bool(args.show_context),
|
||||
show_plan=bool(args.show_plan),
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
|
||||
try:
|
||||
Config.validate_config()
|
||||
except Exception as exc:
|
||||
print(f"Configuration error: {exc}", file=sys.stderr)
|
||||
print("Please check `config/config.ini` and your model/API settings.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.query:
|
||||
return one_shot(args)
|
||||
return interactive_loop(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""简洁演示问数脚本:只保留问题输入与结果输出。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
# 允许直接使用 `python scripts/demo_chat.py` 运行
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from agent.agents.conversation import ConversationAgent
|
||||
from config import Config
|
||||
from scripts.console_chat import extract_table_rows, parse_sr_api_result, render_text_table
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="演示版控制台问数:仅展示耗时、SQL、结果表和数据行数。"
|
||||
)
|
||||
parser.add_argument("--model-section", default=None, help="可选:指定 config.ini 中的模型配置段")
|
||||
parser.add_argument("--conversation-id", default=None, help="可选:会话 ID")
|
||||
parser.add_argument("--user", default="demo-user", help="可选:用户标识")
|
||||
parser.add_argument("--query", "-q", default=None, help="单次执行模式:直接执行一条问题后退出")
|
||||
return parser
|
||||
|
||||
|
||||
def _last_answer_text(result: Dict[str, Any]) -> str:
|
||||
messages = (result or {}).get("messages") or []
|
||||
if not messages:
|
||||
return ""
|
||||
last = messages[-1]
|
||||
return getattr(last, "content", "") or str(last)
|
||||
|
||||
|
||||
def format_demo_result(result: Dict[str, Any], elapsed_seconds: float) -> str:
|
||||
context = (result or {}).get("context") or {}
|
||||
final_sql = str(context.get("final_sql") or "")
|
||||
parsed_result = parse_sr_api_result(context.get("sr_api_result"))
|
||||
headers, rows = extract_table_rows(parsed_result)
|
||||
row_count = len(rows)
|
||||
has_structured_result = context.get("sr_api_result") is not None and isinstance(parsed_result, (dict, list))
|
||||
is_empty_result = bool(context.get("is_empty_result"))
|
||||
response_source = str(context.get("response_source") or "")
|
||||
answer = _last_answer_text(result)
|
||||
|
||||
blocks = [f"耗时: {elapsed_seconds:.2f}s"]
|
||||
|
||||
if final_sql:
|
||||
blocks.append(f"SQL:\n{final_sql}")
|
||||
else:
|
||||
blocks.append("SQL:\n<未生成 SQL>")
|
||||
|
||||
blocks.append(f"数据行数: {row_count}")
|
||||
|
||||
if is_empty_result and response_source in {"model_empty_result_fallback", "empty_result_fixed_fallback"}:
|
||||
blocks.append(f"结果说明:\n{answer or '未查询到符合条件的数据,请尝试调整筛选条件后再查询。'}")
|
||||
elif has_structured_result:
|
||||
table_text = render_text_table(headers or ['result'], rows)
|
||||
blocks.append(f"SQL执行结果表:\n{table_text}")
|
||||
else:
|
||||
blocks.append(f"SQL执行结果:\n{answer or str(parsed_result or '<无结果>')}")
|
||||
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
def run_turn(
|
||||
agent: ConversationAgent,
|
||||
query: str,
|
||||
*,
|
||||
user: str,
|
||||
conversation_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
started_at = time.perf_counter()
|
||||
result = agent.run(
|
||||
query,
|
||||
user=user,
|
||||
conversation_id=conversation_id,
|
||||
skip_sr_api=False,
|
||||
debug_node_trace=False,
|
||||
)
|
||||
elapsed_seconds = time.perf_counter() - started_at
|
||||
print(format_demo_result(result, elapsed_seconds))
|
||||
return result
|
||||
|
||||
|
||||
def interactive_loop(args: argparse.Namespace) -> int:
|
||||
conversation_id = args.conversation_id or f"demo_{uuid.uuid4().hex[:8]}"
|
||||
agent = ConversationAgent(model_section=args.model_section)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n问题> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nBye.")
|
||||
return 0
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input.lower() in {"/quit", "/exit", "quit", "exit"}:
|
||||
print("Bye.")
|
||||
return 0
|
||||
|
||||
try:
|
||||
run_turn(
|
||||
agent,
|
||||
user_input,
|
||||
user=args.user,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"\n[ERROR] {exc}")
|
||||
traceback.print_exc()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def one_shot(args: argparse.Namespace) -> int:
|
||||
conversation_id = args.conversation_id or f"demo_{uuid.uuid4().hex[:8]}"
|
||||
agent = ConversationAgent(model_section=args.model_section)
|
||||
query = str(args.query or "")
|
||||
try:
|
||||
run_turn(
|
||||
agent,
|
||||
query,
|
||||
user=args.user,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
|
||||
try:
|
||||
Config.validate_config()
|
||||
except Exception as exc:
|
||||
print(f"Configuration error: {exc}", file=sys.stderr)
|
||||
print("Please check `config/config.ini` and your model/API settings.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.query:
|
||||
return one_shot(args)
|
||||
return interactive_loop(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
def main():
|
||||
from services.ragflow_sync import RagflowSync
|
||||
from services.integrations.ragflow_sync import RagflowSync
|
||||
syncer = RagflowSync()
|
||||
syncer.sync_table_retrieval()
|
||||
result = syncer.sync_table_retrieval()
|
||||
print("表名检索模板同步完成")
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
from services.ragflow_sync import RagflowSync
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from services.integrations.ragflow_sync import RagflowSync
|
||||
|
||||
|
||||
def main():
|
||||
syncer = RagflowSync()
|
||||
syncer.sync_sql_gen_prompts()
|
||||
result = syncer.sync_sql_gen_prompts()
|
||||
print("SQL 生成提示词同步完成")
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import httpx
|
||||
|
||||
from config import Config
|
||||
|
||||
|
||||
def _base_url() -> str:
|
||||
app = Config.get_section("app")
|
||||
host = app.get("host", "127.0.0.1")
|
||||
port = app.get("port", "8000")
|
||||
if host in ("0.0.0.0", "::"):
|
||||
host = "127.0.0.1"
|
||||
return f"http://{host}:{port}"
|
||||
|
||||
|
||||
def _post(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
|
||||
resp = client.post(url, json=payload)
|
||||
print(f"POST {url} -> {resp.status_code}")
|
||||
print(resp.text)
|
||||
|
||||
|
||||
def _put(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
|
||||
resp = client.put(url, json=payload)
|
||||
print(f"PUT {url} -> {resp.status_code}")
|
||||
print(resp.text)
|
||||
|
||||
|
||||
def _get(client: httpx.Client, url: str) -> None:
|
||||
resp = client.get(url)
|
||||
print(f"GET {url} -> {resp.status_code}")
|
||||
print(resp.text)
|
||||
|
||||
|
||||
def _stream_sse(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
|
||||
with client.stream("POST", url, json=payload) as resp:
|
||||
print(f"POST {url} -> {resp.status_code}")
|
||||
current_event = "message"
|
||||
for raw in resp.iter_lines():
|
||||
if raw is None:
|
||||
continue
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("event:"):
|
||||
current_event = line.split(":", 1)[1].strip() or "message"
|
||||
continue
|
||||
if line.startswith("data:"):
|
||||
data = line.split(":", 1)[1].strip()
|
||||
print(f"[{current_event}] {data}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
base = _base_url()
|
||||
menu: List[str] = [
|
||||
"1) GET /health",
|
||||
"2) GET /nacos/status",
|
||||
"3) POST /api/workflows (conversation)",
|
||||
"4) POST /api/workflows/stream (conversation)",
|
||||
"5) POST /api/sql/generate",
|
||||
"6) POST /api/tools/execute",
|
||||
"7) POST /api/prompts/reload",
|
||||
"8) POST /api/ragflow/table-retrieval/upload",
|
||||
"9) PUT /api/ragflow/table-retrieval/update",
|
||||
"10) POST /api/ragflow/sql-gen/upload",
|
||||
"11) PUT /api/ragflow/sql-gen/update",
|
||||
"0) Exit",
|
||||
]
|
||||
|
||||
with httpx.Client(timeout=60) as client:
|
||||
while True:
|
||||
print("\n可用接口:")
|
||||
for line in menu:
|
||||
print(line)
|
||||
|
||||
choice = input("\n请选择编号: ").strip()
|
||||
if choice == "0":
|
||||
break
|
||||
|
||||
if choice == "1":
|
||||
_get(client, f"{base}/health")
|
||||
elif choice == "2":
|
||||
_get(client, f"{base}/nacos/status")
|
||||
elif choice == "3":
|
||||
payload = {
|
||||
"query": "查询 SO 4020438779 的 eta 信息",
|
||||
"conversation_id": None,
|
||||
"workflow_type": "conversation",
|
||||
"response_mode": "blocking",
|
||||
"user": "tester",
|
||||
"inputs": {},
|
||||
}
|
||||
_post(client, f"{base}/api/workflows", payload)
|
||||
elif choice == "4":
|
||||
payload = {
|
||||
"query": "查询 SO 4016769041 的 eta 信息",
|
||||
"conversation_id": None,
|
||||
"response_mode": "streaming",
|
||||
"user": "tester",
|
||||
"inputs": {},
|
||||
"files": [],
|
||||
}
|
||||
_stream_sse(client, f"{base}/api/workflows/stream", payload)
|
||||
elif choice == "5":
|
||||
payload = {
|
||||
"query": "查询 SO 4020438779 的 eta 信息",
|
||||
"conversation_id": None,
|
||||
"workflow_type": "conversation",
|
||||
"response_mode": "blocking",
|
||||
"user": "tester",
|
||||
"inputs": {},
|
||||
}
|
||||
_post(client, f"{base}/api/sql/generate", payload)
|
||||
elif choice == "6":
|
||||
payload = {
|
||||
"tool_name": "sr_api_query",
|
||||
"payload": {
|
||||
"sql": "SELECT 1",
|
||||
"page": 1,
|
||||
"rows": 1,
|
||||
"orderBySelect": True,
|
||||
"timeout": 30,
|
||||
},
|
||||
}
|
||||
_post(client, f"{base}/api/tools/execute", payload)
|
||||
elif choice == "7":
|
||||
_post(client, f"{base}/api/prompts/reload", {})
|
||||
elif choice == "8":
|
||||
_post(client, f"{base}/api/ragflow/table-retrieval/upload", {})
|
||||
elif choice == "9":
|
||||
cfg = {"name": "table_retrieval_dataset"}
|
||||
_put(client, f"{base}/api/ragflow/table-retrieval/update", cfg)
|
||||
elif choice == "10":
|
||||
_post(client, f"{base}/api/ragflow/sql-gen/upload", {})
|
||||
elif choice == "11":
|
||||
cfg = {"name": "sql_gen_dataset"}
|
||||
_put(client, f"{base}/api/ragflow/sql-gen/update", cfg)
|
||||
else:
|
||||
print("无效选择")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SQL 提示词 Redis 热更新脚本
|
||||
|
||||
用法:
|
||||
python scripts/update_sql_prompts.py # 同步所有本地文件到 Redis
|
||||
python scripts/update_sql_prompts.py --tables apbo_eta_ful apbo_eta_milestone # 同步指定表
|
||||
python scripts/update_sql_prompts.py --list # 列出 Redis 中的所有表
|
||||
python scripts/update_sql_prompts.py --delete apbo_eta_ful # 删除指定表
|
||||
python scripts/update_sql_prompts.py --from-file path/to/file.json --table apbo_eta_ful # 从指定文件更新
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from config import Config
|
||||
from services.storage.cache import RedisCache
|
||||
|
||||
|
||||
def _first_line(value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
text = str(value)
|
||||
return text.splitlines()[0].strip() if text else ""
|
||||
|
||||
|
||||
def _to_bool(value, default: bool = False) -> bool:
|
||||
text = _first_line(value).lower()
|
||||
if not text:
|
||||
return default
|
||||
return text in ("1", "true", "yes", "y", "on")
|
||||
|
||||
|
||||
def _to_int(value, default: int = 0) -> int:
|
||||
text = _first_line(value)
|
||||
if not text:
|
||||
return default
|
||||
try:
|
||||
return int(text)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def get_redis_cache() -> RedisCache:
|
||||
redis_cfg = Config.get_section("redis")
|
||||
enabled = _to_bool(redis_cfg.get("enabled", "false"))
|
||||
if not enabled:
|
||||
raise RuntimeError("Redis 未启用,请检查配置 redis.enabled")
|
||||
|
||||
url = _first_line(redis_cfg.get("url"))
|
||||
db = _to_int(redis_cfg.get("db", redis_cfg.get("database", 0)), default=0)
|
||||
if not url:
|
||||
host = _first_line(redis_cfg.get("host"))
|
||||
port = _first_line(redis_cfg.get("port", "6379")) or "6379"
|
||||
password = _first_line(redis_cfg.get("password", ""))
|
||||
username = _first_line(redis_cfg.get("username", ""))
|
||||
database = _first_line(redis_cfg.get("database", str(db))) or str(db)
|
||||
if host:
|
||||
from urllib.parse import quote_plus
|
||||
if username and password:
|
||||
auth = f"{quote_plus(username)}:{quote_plus(password)}@"
|
||||
elif password:
|
||||
auth = f":{quote_plus(password)}@"
|
||||
else:
|
||||
auth = ""
|
||||
url = f"redis://{auth}{host}:{port}/{database}"
|
||||
|
||||
if not url:
|
||||
raise RuntimeError("Redis 配置不完整,请检查 redis.url 或 redis.host")
|
||||
|
||||
return RedisCache(url=url, db=db)
|
||||
|
||||
|
||||
def get_ttl() -> int:
|
||||
redis_cfg = Config.get_section("redis")
|
||||
return _to_int(redis_cfg.get("sql_prompt_ttl", 0), default=0)
|
||||
|
||||
|
||||
def sync_from_local_files(cache: RedisCache, tables: list = None, ttl: int = None):
|
||||
prompts_dir = PROJECT_ROOT / "config" / "sql_gen_prompts"
|
||||
|
||||
if tables:
|
||||
files = [prompts_dir / f"{t}.json" for t in tables]
|
||||
else:
|
||||
files = list(prompts_dir.glob("*.json"))
|
||||
|
||||
results = {}
|
||||
for file_path in files:
|
||||
if not file_path.exists():
|
||||
print(f"[跳过] 文件不存在: {file_path}")
|
||||
continue
|
||||
|
||||
table_name = file_path.stem
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
prompt = json.load(f)
|
||||
|
||||
key = f"sql_prompt:{table_name}"
|
||||
cache.set(key, json.dumps(prompt, ensure_ascii=False), ttl)
|
||||
results[table_name] = "success"
|
||||
print(f"[成功] {table_name}")
|
||||
except Exception as e:
|
||||
results[table_name] = f"failed: {e}"
|
||||
print(f"[失败] {table_name}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def update_from_file(cache: RedisCache, file_path: str, table_name: str, ttl: int = None):
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f"[错误] 文件不存在: {file_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
prompt = json.load(f)
|
||||
|
||||
key = f"sql_prompt:{table_name}"
|
||||
cache.set(key, json.dumps(prompt, ensure_ascii=False), ttl)
|
||||
print(f"[成功] 已更新 {table_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[失败] {table_name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def list_tables(cache: RedisCache):
|
||||
keys = cache.keys("sql_prompt:*")
|
||||
tables = []
|
||||
for key in keys:
|
||||
parts = key.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] != "table_list":
|
||||
tables.append(parts[1])
|
||||
|
||||
if tables:
|
||||
print("Redis 中的 SQL 提示词表:")
|
||||
for t in sorted(tables):
|
||||
print(f" - {t}")
|
||||
else:
|
||||
print("Redis 中没有 SQL 提示词")
|
||||
return tables
|
||||
|
||||
|
||||
def delete_table(cache: RedisCache, table_name: str):
|
||||
key = f"sql_prompt:{table_name}"
|
||||
cache.delete(key)
|
||||
print(f"[成功] 已删除 {table_name}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SQL 提示词 Redis 热更新工具")
|
||||
parser.add_argument("--tables", nargs="*", help="指定要同步的表名列表")
|
||||
parser.add_argument("--list", action="store_true", help="列出 Redis 中的所有表")
|
||||
parser.add_argument("--delete", type=str, help="删除指定表")
|
||||
parser.add_argument("--from-file", type=str, help="从指定文件更新")
|
||||
parser.add_argument("--table", type=str, help="目标表名(与 --from-file 配合使用)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
cache = get_redis_cache()
|
||||
ttl = get_ttl()
|
||||
except Exception as e:
|
||||
print(f"[错误] {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.list:
|
||||
list_tables(cache)
|
||||
elif args.delete:
|
||||
delete_table(cache, args.delete)
|
||||
elif args.from_file:
|
||||
if not args.table:
|
||||
print("[错误] 使用 --from-file 时必须指定 --table")
|
||||
sys.exit(1)
|
||||
update_from_file(cache, args.from_file, args.table, ttl)
|
||||
else:
|
||||
sync_from_local_files(cache, args.tables, ttl)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user