Name | ai-runner-hjy JSON |
Version |
0.0.1
JSON |
| download |
home_page | None |
Summary | DB-config driven OpenAI-compatible caller with auditing (RDS logs), CLI + import API |
upload_time | 2025-08-09 12:20:23 |
maintainer | None |
docs_url | None |
author | hjy |
requires_python | >=3.9 |
license | MIT |
keywords |
openai
chat
audit
mysql
langgraph
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# ai_runner_hjy(AI 调用装配与审计模块)
> 注意(必须阅读):本模块“只会”从【项目根目录】加载 env 文件(`basic.env`、`mysql.env`)。子目录中的任何 `*.env` 仅作为模板示例,运行期不会读取。
> 运行前会强校验以下必填项,缺失将直接报错并终止:
> - `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`
> - 二选一:`MYSQL_AI_DATABASE` 或 `MYSQL_DATABASE`
> - `AI_PEPPER`
> 若缺失,会得到类似错误:
> `RuntimeError: Missing required env variables: MYSQL_HOST, AI_PEPPER. Ensure you have created root-level basic.env/mysql.env and filled values.`
一个可复用的小模块:以“数据库配置驱动”的方式装配并调用 OpenAI Chat Completions 兼容接口(如 qdd/openrouter),并将调用指标与请求/响应 JSON 统一写入 RDS 以便审计与回放。
---
## 适用场景
- 需要“配置切换、代码零改动”的 AI 调用范式(模型/参数/Prompt 全在库里配置)。
- 需要强约束的日志与审计(耗时、状态、token 用量、请求 JSON、响应 JSON)。
- 多项目共用同一套 AI 连接/模型配置,或需要运维同学集中管理配置。
---
## 核心能力
- 按 `config_key` 从 RDS 读取:`ai_connection` → `ai_model` → `ai_param_profile` → `ai_prompt` → `ai_config`。
- 运行时使用 `AI_PEPPER` 在内存里解密 API Key(RDS 仅存 AES‑GCM 密文 JSON)。
- 组装 OpenAI Chat Completions 请求体,参数白名单映射:
- `TEMPERATURE→temperature`,`TOP_P→top_p`,`MAX_TOKENS→max_tokens`,`STREAM→stream`,`STOP→stop`,`N→n`,`FREQUENCY_PENALTY→frequency_penalty`,`PRESENCE_PENALTY→presence_penalty`
- 若 `JSON_SCHEMA_ENFORCE=true` 且未在 Prompt 指定 `response_format`,兜底为 `{type:json_object}`
- 统一入库 `ai_call_logs`:
- 最小:`http_status`、`duration_ms`、token 用量、`response_id`、错误码/信息
- 扩展:`request_body_json`、`response_body_json`(可选大 JSON;若库未加列则自动回退,只写最小指标)
---
## 目录结构
- `runner.py`:装配 → 调用 → 入库(主要入口)
- `crypto_utils.py`:API Key 解密(AES‑256‑GCM + PBKDF2)
- `core/crud.py`:最小 CRUD(示例实现 `ai_param_profile` 的新增/查询,带幂等校验+统一错误返回)
- `__init__.py`:导出 `run_once / load_envs / get_db_connection`
---
## 环境准备
- 依赖安装(清华源):
```bash
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r backend/requirements.txt
```
- 环境变量(模块会从项目根目录按需加载 `basic.env`、`mysql.env`,不覆盖已有环境变量):
- 必填:`MYSQL_HOST` `MYSQL_PORT` `MYSQL_USER` `MYSQL_PASSWORD` `MYSQL_AI_DATABASE`(或 `MYSQL_DATABASE`)
- 必填:`AI_PEPPER`(Pepper,用于从密文 JSON 解密 API Key;严禁入库/泄露)
---
## 最小数据库结构(可直接执行)
> 为了可审计与回放,推荐包含请求/响应两列。若暂不需要,可先省略两列,模块会自动回退到最小日志模式。
```sql
-- 1) 连接表:只存密文 JSON
CREATE TABLE IF NOT EXISTS ai_connection (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
provider ENUM('openai','openrouter','qdd','google','custom') NOT NULL,
base_url VARCHAR(500) NOT NULL,
api_key_encrypted JSON NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_conn_name (name)
);
-- 2) 模型:指向连接
CREATE TABLE IF NOT EXISTS ai_model (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(120) NOT NULL,
connection_id BIGINT NOT NULL,
defaults_json JSON NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_model_name (name),
KEY idx_model_connection (connection_id),
CONSTRAINT fk_model_connection FOREIGN KEY (connection_id) REFERENCES ai_connection(id)
);
-- 3) 参数白名单(映射到 OpenAI body)
CREATE TABLE IF NOT EXISTS ai_param_profile (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(120) NOT NULL,
params_json JSON NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_param_name (name)
);
-- 4) Prompt(可含 variables_json 占位符)
CREATE TABLE IF NOT EXISTS ai_prompt (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(120) NOT NULL,
version VARCHAR(20) NOT NULL,
description TEXT NULL,
messages_json JSON NOT NULL,
response_format_json JSON NULL,
variables_json JSON NULL,
status ENUM('active','archived') NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_prompt_name_version (name, version)
);
-- 5) 配置入口:以 config_key 作为对外接口
CREATE TABLE IF NOT EXISTS ai_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
config_key VARCHAR(120) NOT NULL,
model_id BIGINT NOT NULL,
param_profile_id BIGINT NULL,
prompt_id BIGINT NULL,
description TEXT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_config_key (config_key),
KEY idx_config_model (model_id),
KEY idx_config_param (param_profile_id),
KEY idx_config_prompt (prompt_id),
CONSTRAINT fk_cfg_model FOREIGN KEY (model_id) REFERENCES ai_model(id),
CONSTRAINT fk_cfg_param FOREIGN KEY (param_profile_id) REFERENCES ai_param_profile(id),
CONSTRAINT fk_cfg_prompt FOREIGN KEY (prompt_id) REFERENCES ai_prompt(id)
);
-- 6) 调用日志:最小指标 + 可选大 JSON
CREATE TABLE IF NOT EXISTS ai_call_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
config_key VARCHAR(120) NOT NULL,
http_status INT NOT NULL,
duration_ms INT NOT NULL,
prompt_tokens INT NULL,
completion_tokens INT NULL,
total_tokens INT NULL,
response_id VARCHAR(128) NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(512) NULL,
request_body_json JSON NULL,
response_body_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_logs_cfg (config_key),
KEY idx_logs_time (created_at)
);
```
> 迁移提示(已有库需要补齐唯一约束时):
> 如已存在 `ai_config` 表但尚未对 `config_key` 加唯一索引,可执行:
>
> ```sql
> ALTER TABLE ai_config.ai_config ADD UNIQUE KEY uk_ai_config_config_key (config_key);
> ```
> 执行后用 `SHOW INDEX FROM ai_config.ai_config` 验证 `Non_unique=0`、`Key_name=uk_ai_config_config_key`、`Column_name=config_key`。
> API Key 加密请使用与本模块一致的 AES‑GCM 方案(见 `crypto_utils.py`),`ai_connection.api_key_encrypted` 列保存密文 JSON:`{"v","alg","iter","salt","nonce","ct"}`(可带 `tag` 兼容旧格式)。
---
## 种子数据(示例)
以 openrouter 为例(仅示意,实际请根据你的提供商修改)。
```sql
-- 连接(先插空密文,随后再 UPDATE 为真实密文)
INSERT INTO ai_connection (name, provider, base_url, api_key_encrypted, is_active)
VALUES ('openrouter_default', 'openrouter', 'https://openrouter.ai/api/v1/chat/completions', JSON_OBJECT(), 1);
-- 模型
ingert INTO ai_model (name, connection_id, is_active)
SELECT 'gemini-2.5-flash', id, 1 FROM ai_connection WHERE name='openrouter_default';
-- 参数白名单
ingert INTO ai_param_profile (name, params_json, is_active)
VALUES ('default_json', JSON_OBJECT('TEMPERATURE',0.2,'MAX_TOKENS',256,'JSON_SCHEMA_ENFORCE', true), 1);
-- Prompt(简单单轮)
INSERT INTO ai_prompt (name, version, description, messages_json, response_format_json, variables_json, status)
VALUES (
'single_chat_zh','v1','单轮对话 JSON 输出',
JSON_ARRAY(
JSON_OBJECT('role','system','content','只输出 JSON,不要多余文本'),
JSON_OBJECT('role','user','content', JSON_OBJECT('question','{{QUESTION}}'))
),
JSON_OBJECT('type','json_object'),
JSON_OBJECT('QUESTION','给我一句鼓励的话'),
'active'
);
-- 配置入口(对外只暴露这个 key)
INSERT INTO ai_config (config_key, model_id, param_profile_id, prompt_id, description, is_active)
SELECT 'gemini25_single_chat', m.id, p.id, pr.id, 'gemini-2.5-flash 单轮对话', 1
FROM ai_model m, ai_param_profile p, ai_prompt pr
WHERE m.name='gemini-2.5-flash' AND p.name='default_json' AND pr.name='single_chat_zh' AND pr.version='v1';
```
> 记得用 `crypto_utils.py` 相同算法生成密文 JSON,并 `UPDATE ai_connection.api_key_encrypted`。
---
## 变量替换(占位符)
- 在 `ai_prompt.variables_json` 与/或 `ai_param_profile.params_json` 中提供变量,模块会按 `{{VARIABLE}}` 替换到 `messages_json`。
- 多模态占位示意:`{{IMAGE_URL}}` / `{{AUDIO_URL}}` / `{{VIDEO_URL}}`(取决于你的消息结构)。
---
## 快速开始
```python
from backend.ai_runner_hjy import run_once
import os
# 方式一:通过环境变量指定(脚本/任务方便)
os.environ['AI_TEST_CONFIG_KEY'] = 'gemini25_single_chat'
run_once()
# 方式二:在代码里显式传入(服务内调用)
run_once('gemini25_single_chat')
```
### 命令行(CLI)用法
无需改代码,直接运行一次:
```bash
python -m backend.ai_runner_hjy --config-key gemini25_single_chat
# 可选参数
python -m backend.ai_runner_hjy --config-key gemini25_single_chat --timeout 30 --max-retries 2
```
### CRUD 最小用法(示例)
```python
from backend.ai_runner_hjy.core.crud import add_param_profile_if_absent, get_param_profile_by_name
# 幂等新增(存在则返回 existed=True)
res, err = add_param_profile_if_absent("default_json", {"TEMPERATURE": 0.2, "MAX_TOKENS": 256})
if err:
# err 为 (code, message),例如 ("DB_ERROR", "...") / ("VALIDATION_ERROR", "...")
raise RuntimeError(err)
print(res) # {"id": 123, "existed": False}
# 查询
row, err = get_param_profile_by_name("default_json")
if err:
raise RuntimeError(err)
print(row["params_json"]) # JSON 字符串
```
---
## 日志与错误处理
- 每次调用必写一行 `ai_call_logs`:
- 成功:`http_status=200/201`、`duration_ms`、`usage.*`、`response_id`,以及两列 JSON(若存在)
- 失败:`error_code` 为 `HTTP_STATUS_ERROR`(非 2xx)或 `REQUEST_ERROR`(网络/序列化),同时尽量保留响应 JSON 便于排障
- 建议定期审计:慢请求、非 2xx 比例、token 消耗
---
## 常见问题(FAQ)
- Q:为什么没有写入 `request_body_json/response_body_json`?
- A:请确认你已在 `ai_call_logs` 新增两列;否则模块会回退到最小日志模式(不报错)。
- Q:如何强制只输出 JSON?
- A:在 Prompt 的 `response_format_json` 指定 `{type:"json_object"}`,或在参数白名单设置 `JSON_SCHEMA_ENFORCE=true`(兜底)。
- Q:API Key 放哪里?
- A:只以密文 JSON 存在 `ai_connection.api_key_encrypted`。运行时用环境变量 `AI_PEPPER` 解密,明文不会落库/日志。
---
## 与服务集成(示例)
```python
from backend.ai_runner_hjy import run_once
def handle_request(config_key: str):
# … 业务逻辑
run_once(config_key)
```
> 若你以前通过 `subprocess` 调脚本,现在可以直接导入函数调用,便于单测与复用。
---
## 许可证与复用
- 内部脚手架,依赖最小、接口清晰,便于在不同项目迁移复用。欢迎在此基础上扩展:
- 增加缓存/重试/降级策略参数
- 将请求/响应落 OSS(大体量)并在日志里保存链接
- 接入更完整的观测与指标
Raw data
{
"_id": null,
"home_page": null,
"name": "ai-runner-hjy",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "openai, chat, audit, mysql, langgraph",
"author": "hjy",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/7a/d2/51bd7935521bf83402de5c30bfd805a1ce32d412a78946e0e3284bd142a4/ai_runner_hjy-0.0.1.tar.gz",
"platform": null,
"description": "# ai_runner_hjy\uff08AI \u8c03\u7528\u88c5\u914d\u4e0e\u5ba1\u8ba1\u6a21\u5757\uff09\n\n> \u6ce8\u610f\uff08\u5fc5\u987b\u9605\u8bfb\uff09\uff1a\u672c\u6a21\u5757\u201c\u53ea\u4f1a\u201d\u4ece\u3010\u9879\u76ee\u6839\u76ee\u5f55\u3011\u52a0\u8f7d env \u6587\u4ef6\uff08`basic.env`\u3001`mysql.env`\uff09\u3002\u5b50\u76ee\u5f55\u4e2d\u7684\u4efb\u4f55 `*.env` \u4ec5\u4f5c\u4e3a\u6a21\u677f\u793a\u4f8b\uff0c\u8fd0\u884c\u671f\u4e0d\u4f1a\u8bfb\u53d6\u3002\n> \u8fd0\u884c\u524d\u4f1a\u5f3a\u6821\u9a8c\u4ee5\u4e0b\u5fc5\u586b\u9879\uff0c\u7f3a\u5931\u5c06\u76f4\u63a5\u62a5\u9519\u5e76\u7ec8\u6b62\uff1a\n> - `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`\n> - \u4e8c\u9009\u4e00\uff1a`MYSQL_AI_DATABASE` \u6216 `MYSQL_DATABASE`\n> - `AI_PEPPER`\n> \u82e5\u7f3a\u5931\uff0c\u4f1a\u5f97\u5230\u7c7b\u4f3c\u9519\u8bef\uff1a\n> `RuntimeError: Missing required env variables: MYSQL_HOST, AI_PEPPER. Ensure you have created root-level basic.env/mysql.env and filled values.`\n\n\u4e00\u4e2a\u53ef\u590d\u7528\u7684\u5c0f\u6a21\u5757\uff1a\u4ee5\u201c\u6570\u636e\u5e93\u914d\u7f6e\u9a71\u52a8\u201d\u7684\u65b9\u5f0f\u88c5\u914d\u5e76\u8c03\u7528 OpenAI Chat Completions \u517c\u5bb9\u63a5\u53e3\uff08\u5982 qdd/openrouter\uff09\uff0c\u5e76\u5c06\u8c03\u7528\u6307\u6807\u4e0e\u8bf7\u6c42/\u54cd\u5e94 JSON \u7edf\u4e00\u5199\u5165 RDS \u4ee5\u4fbf\u5ba1\u8ba1\u4e0e\u56de\u653e\u3002\n\n---\n\n## \u9002\u7528\u573a\u666f\n- \u9700\u8981\u201c\u914d\u7f6e\u5207\u6362\u3001\u4ee3\u7801\u96f6\u6539\u52a8\u201d\u7684 AI \u8c03\u7528\u8303\u5f0f\uff08\u6a21\u578b/\u53c2\u6570/Prompt \u5168\u5728\u5e93\u91cc\u914d\u7f6e\uff09\u3002\n- \u9700\u8981\u5f3a\u7ea6\u675f\u7684\u65e5\u5fd7\u4e0e\u5ba1\u8ba1\uff08\u8017\u65f6\u3001\u72b6\u6001\u3001token \u7528\u91cf\u3001\u8bf7\u6c42 JSON\u3001\u54cd\u5e94 JSON\uff09\u3002\n- \u591a\u9879\u76ee\u5171\u7528\u540c\u4e00\u5957 AI \u8fde\u63a5/\u6a21\u578b\u914d\u7f6e\uff0c\u6216\u9700\u8981\u8fd0\u7ef4\u540c\u5b66\u96c6\u4e2d\u7ba1\u7406\u914d\u7f6e\u3002\n\n---\n\n## \u6838\u5fc3\u80fd\u529b\n- \u6309 `config_key` \u4ece RDS \u8bfb\u53d6\uff1a`ai_connection` \u2192 `ai_model` \u2192 `ai_param_profile` \u2192 `ai_prompt` \u2192 `ai_config`\u3002\n- \u8fd0\u884c\u65f6\u4f7f\u7528 `AI_PEPPER` \u5728\u5185\u5b58\u91cc\u89e3\u5bc6 API Key\uff08RDS \u4ec5\u5b58 AES\u2011GCM \u5bc6\u6587 JSON\uff09\u3002\n- \u7ec4\u88c5 OpenAI Chat Completions \u8bf7\u6c42\u4f53\uff0c\u53c2\u6570\u767d\u540d\u5355\u6620\u5c04\uff1a\n - `TEMPERATURE\u2192temperature`\uff0c`TOP_P\u2192top_p`\uff0c`MAX_TOKENS\u2192max_tokens`\uff0c`STREAM\u2192stream`\uff0c`STOP\u2192stop`\uff0c`N\u2192n`\uff0c`FREQUENCY_PENALTY\u2192frequency_penalty`\uff0c`PRESENCE_PENALTY\u2192presence_penalty`\n - \u82e5 `JSON_SCHEMA_ENFORCE=true` \u4e14\u672a\u5728 Prompt \u6307\u5b9a `response_format`\uff0c\u515c\u5e95\u4e3a `{type:json_object}`\n- \u7edf\u4e00\u5165\u5e93 `ai_call_logs`\uff1a\n - \u6700\u5c0f\uff1a`http_status`\u3001`duration_ms`\u3001token \u7528\u91cf\u3001`response_id`\u3001\u9519\u8bef\u7801/\u4fe1\u606f\n - \u6269\u5c55\uff1a`request_body_json`\u3001`response_body_json`\uff08\u53ef\u9009\u5927 JSON\uff1b\u82e5\u5e93\u672a\u52a0\u5217\u5219\u81ea\u52a8\u56de\u9000\uff0c\u53ea\u5199\u6700\u5c0f\u6307\u6807\uff09\n\n---\n\n## \u76ee\u5f55\u7ed3\u6784\n- `runner.py`\uff1a\u88c5\u914d \u2192 \u8c03\u7528 \u2192 \u5165\u5e93\uff08\u4e3b\u8981\u5165\u53e3\uff09\n- `crypto_utils.py`\uff1aAPI Key \u89e3\u5bc6\uff08AES\u2011256\u2011GCM + PBKDF2\uff09\n- `core/crud.py`\uff1a\u6700\u5c0f CRUD\uff08\u793a\u4f8b\u5b9e\u73b0 `ai_param_profile` \u7684\u65b0\u589e/\u67e5\u8be2\uff0c\u5e26\u5e42\u7b49\u6821\u9a8c+\u7edf\u4e00\u9519\u8bef\u8fd4\u56de\uff09\n- `__init__.py`\uff1a\u5bfc\u51fa `run_once / load_envs / get_db_connection`\n\n---\n\n## \u73af\u5883\u51c6\u5907\n- \u4f9d\u8d56\u5b89\u88c5\uff08\u6e05\u534e\u6e90\uff09\uff1a\n```bash\npip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r backend/requirements.txt\n```\n- \u73af\u5883\u53d8\u91cf\uff08\u6a21\u5757\u4f1a\u4ece\u9879\u76ee\u6839\u76ee\u5f55\u6309\u9700\u52a0\u8f7d `basic.env`\u3001`mysql.env`\uff0c\u4e0d\u8986\u76d6\u5df2\u6709\u73af\u5883\u53d8\u91cf\uff09\uff1a\n - \u5fc5\u586b\uff1a`MYSQL_HOST` `MYSQL_PORT` `MYSQL_USER` `MYSQL_PASSWORD` `MYSQL_AI_DATABASE`(\u6216 `MYSQL_DATABASE`)\n - \u5fc5\u586b\uff1a`AI_PEPPER`\uff08Pepper\uff0c\u7528\u4e8e\u4ece\u5bc6\u6587 JSON \u89e3\u5bc6 API Key\uff1b\u4e25\u7981\u5165\u5e93/\u6cc4\u9732\uff09\n\n---\n\n## \u6700\u5c0f\u6570\u636e\u5e93\u7ed3\u6784\uff08\u53ef\u76f4\u63a5\u6267\u884c\uff09\n> \u4e3a\u4e86\u53ef\u5ba1\u8ba1\u4e0e\u56de\u653e\uff0c\u63a8\u8350\u5305\u542b\u8bf7\u6c42/\u54cd\u5e94\u4e24\u5217\u3002\u82e5\u6682\u4e0d\u9700\u8981\uff0c\u53ef\u5148\u7701\u7565\u4e24\u5217\uff0c\u6a21\u5757\u4f1a\u81ea\u52a8\u56de\u9000\u5230\u6700\u5c0f\u65e5\u5fd7\u6a21\u5f0f\u3002\n\n```sql\n-- 1) \u8fde\u63a5\u8868\uff1a\u53ea\u5b58\u5bc6\u6587 JSON\nCREATE TABLE IF NOT EXISTS ai_connection (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n name VARCHAR(100) NOT NULL,\n provider ENUM('openai','openrouter','qdd','google','custom') NOT NULL,\n base_url VARCHAR(500) NOT NULL,\n api_key_encrypted JSON NOT NULL,\n is_active TINYINT(1) NOT NULL DEFAULT 1,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY uk_conn_name (name)\n);\n\n-- 2) \u6a21\u578b\uff1a\u6307\u5411\u8fde\u63a5\nCREATE TABLE IF NOT EXISTS ai_model (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n name VARCHAR(120) NOT NULL,\n connection_id BIGINT NOT NULL,\n defaults_json JSON NULL,\n is_active TINYINT(1) NOT NULL DEFAULT 1,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY uk_model_name (name),\n KEY idx_model_connection (connection_id),\n CONSTRAINT fk_model_connection FOREIGN KEY (connection_id) REFERENCES ai_connection(id)\n);\n\n-- 3) \u53c2\u6570\u767d\u540d\u5355\uff08\u6620\u5c04\u5230 OpenAI body\uff09\nCREATE TABLE IF NOT EXISTS ai_param_profile (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n name VARCHAR(120) NOT NULL,\n params_json JSON NOT NULL,\n is_active TINYINT(1) NOT NULL DEFAULT 1,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY uk_param_name (name)\n);\n\n-- 4) Prompt\uff08\u53ef\u542b variables_json \u5360\u4f4d\u7b26\uff09\nCREATE TABLE IF NOT EXISTS ai_prompt (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n name VARCHAR(120) NOT NULL,\n version VARCHAR(20) NOT NULL,\n description TEXT NULL,\n messages_json JSON NOT NULL,\n response_format_json JSON NULL,\n variables_json JSON NULL,\n status ENUM('active','archived') NOT NULL DEFAULT 'active',\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY uk_prompt_name_version (name, version)\n);\n\n-- 5) \u914d\u7f6e\u5165\u53e3\uff1a\u4ee5 config_key \u4f5c\u4e3a\u5bf9\u5916\u63a5\u53e3\nCREATE TABLE IF NOT EXISTS ai_config (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n config_key VARCHAR(120) NOT NULL,\n model_id BIGINT NOT NULL,\n param_profile_id BIGINT NULL,\n prompt_id BIGINT NULL,\n description TEXT NULL,\n is_active TINYINT(1) NOT NULL DEFAULT 1,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY uk_config_key (config_key),\n KEY idx_config_model (model_id),\n KEY idx_config_param (param_profile_id),\n KEY idx_config_prompt (prompt_id),\n CONSTRAINT fk_cfg_model FOREIGN KEY (model_id) REFERENCES ai_model(id),\n CONSTRAINT fk_cfg_param FOREIGN KEY (param_profile_id) REFERENCES ai_param_profile(id),\n CONSTRAINT fk_cfg_prompt FOREIGN KEY (prompt_id) REFERENCES ai_prompt(id)\n);\n\n-- 6) \u8c03\u7528\u65e5\u5fd7\uff1a\u6700\u5c0f\u6307\u6807 + \u53ef\u9009\u5927 JSON\nCREATE TABLE IF NOT EXISTS ai_call_logs (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n config_key VARCHAR(120) NOT NULL,\n http_status INT NOT NULL,\n duration_ms INT NOT NULL,\n prompt_tokens INT NULL,\n completion_tokens INT NULL,\n total_tokens INT NULL,\n response_id VARCHAR(128) NULL,\n error_code VARCHAR(64) NULL,\n error_message VARCHAR(512) NULL,\n request_body_json JSON NULL,\n response_body_json JSON NULL,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n KEY idx_logs_cfg (config_key),\n KEY idx_logs_time (created_at)\n);\n```\n\n> \u8fc1\u79fb\u63d0\u793a\uff08\u5df2\u6709\u5e93\u9700\u8981\u8865\u9f50\u552f\u4e00\u7ea6\u675f\u65f6\uff09\uff1a\n> \u5982\u5df2\u5b58\u5728 `ai_config` \u8868\u4f46\u5c1a\u672a\u5bf9 `config_key` \u52a0\u552f\u4e00\u7d22\u5f15\uff0c\u53ef\u6267\u884c\uff1a\n>\n> ```sql\n> ALTER TABLE ai_config.ai_config ADD UNIQUE KEY uk_ai_config_config_key (config_key);\n> ```\n> \u6267\u884c\u540e\u7528 `SHOW INDEX FROM ai_config.ai_config` \u9a8c\u8bc1 `Non_unique=0`\u3001`Key_name=uk_ai_config_config_key`\u3001`Column_name=config_key`\u3002\n\n> API Key \u52a0\u5bc6\u8bf7\u4f7f\u7528\u4e0e\u672c\u6a21\u5757\u4e00\u81f4\u7684 AES\u2011GCM \u65b9\u6848\uff08\u89c1 `crypto_utils.py`\uff09\uff0c`ai_connection.api_key_encrypted` \u5217\u4fdd\u5b58\u5bc6\u6587 JSON\uff1a`{\"v\",\"alg\",\"iter\",\"salt\",\"nonce\",\"ct\"}`\uff08\u53ef\u5e26 `tag` \u517c\u5bb9\u65e7\u683c\u5f0f\uff09\u3002\n\n---\n\n## \u79cd\u5b50\u6570\u636e\uff08\u793a\u4f8b\uff09\n\u4ee5 openrouter \u4e3a\u4f8b\uff08\u4ec5\u793a\u610f\uff0c\u5b9e\u9645\u8bf7\u6839\u636e\u4f60\u7684\u63d0\u4f9b\u5546\u4fee\u6539\uff09\u3002\n```sql\n-- \u8fde\u63a5\uff08\u5148\u63d2\u7a7a\u5bc6\u6587\uff0c\u968f\u540e\u518d UPDATE \u4e3a\u771f\u5b9e\u5bc6\u6587\uff09\nINSERT INTO ai_connection (name, provider, base_url, api_key_encrypted, is_active)\nVALUES ('openrouter_default', 'openrouter', 'https://openrouter.ai/api/v1/chat/completions', JSON_OBJECT(), 1);\n\n-- \u6a21\u578b\ningert INTO ai_model (name, connection_id, is_active)\nSELECT 'gemini-2.5-flash', id, 1 FROM ai_connection WHERE name='openrouter_default';\n\n-- \u53c2\u6570\u767d\u540d\u5355\ningert INTO ai_param_profile (name, params_json, is_active)\nVALUES ('default_json', JSON_OBJECT('TEMPERATURE',0.2,'MAX_TOKENS',256,'JSON_SCHEMA_ENFORCE', true), 1);\n\n-- Prompt\uff08\u7b80\u5355\u5355\u8f6e\uff09\nINSERT INTO ai_prompt (name, version, description, messages_json, response_format_json, variables_json, status)\nVALUES (\n 'single_chat_zh','v1','\u5355\u8f6e\u5bf9\u8bdd JSON \u8f93\u51fa',\n JSON_ARRAY(\n JSON_OBJECT('role','system','content','\u53ea\u8f93\u51fa JSON\uff0c\u4e0d\u8981\u591a\u4f59\u6587\u672c'),\n JSON_OBJECT('role','user','content', JSON_OBJECT('question','{{QUESTION}}'))\n ),\n JSON_OBJECT('type','json_object'),\n JSON_OBJECT('QUESTION','\u7ed9\u6211\u4e00\u53e5\u9f13\u52b1\u7684\u8bdd'),\n 'active'\n);\n\n-- \u914d\u7f6e\u5165\u53e3\uff08\u5bf9\u5916\u53ea\u66b4\u9732\u8fd9\u4e2a key\uff09\nINSERT INTO ai_config (config_key, model_id, param_profile_id, prompt_id, description, is_active)\nSELECT 'gemini25_single_chat', m.id, p.id, pr.id, 'gemini-2.5-flash \u5355\u8f6e\u5bf9\u8bdd', 1\nFROM ai_model m, ai_param_profile p, ai_prompt pr\nWHERE m.name='gemini-2.5-flash' AND p.name='default_json' AND pr.name='single_chat_zh' AND pr.version='v1';\n```\n> \u8bb0\u5f97\u7528 `crypto_utils.py` \u76f8\u540c\u7b97\u6cd5\u751f\u6210\u5bc6\u6587 JSON\uff0c\u5e76 `UPDATE ai_connection.api_key_encrypted`\u3002\n\n---\n\n## \u53d8\u91cf\u66ff\u6362\uff08\u5360\u4f4d\u7b26\uff09\n- \u5728 `ai_prompt.variables_json` \u4e0e/\u6216 `ai_param_profile.params_json` \u4e2d\u63d0\u4f9b\u53d8\u91cf\uff0c\u6a21\u5757\u4f1a\u6309 `{{VARIABLE}}` \u66ff\u6362\u5230 `messages_json`\u3002\n- \u591a\u6a21\u6001\u5360\u4f4d\u793a\u610f\uff1a`{{IMAGE_URL}}` / `{{AUDIO_URL}}` / `{{VIDEO_URL}}`\uff08\u53d6\u51b3\u4e8e\u4f60\u7684\u6d88\u606f\u7ed3\u6784\uff09\u3002\n\n---\n\n## \u5feb\u901f\u5f00\u59cb\n```python\nfrom backend.ai_runner_hjy import run_once\nimport os\n\n# \u65b9\u5f0f\u4e00\uff1a\u901a\u8fc7\u73af\u5883\u53d8\u91cf\u6307\u5b9a\uff08\u811a\u672c/\u4efb\u52a1\u65b9\u4fbf\uff09\nos.environ['AI_TEST_CONFIG_KEY'] = 'gemini25_single_chat'\nrun_once()\n\n# \u65b9\u5f0f\u4e8c\uff1a\u5728\u4ee3\u7801\u91cc\u663e\u5f0f\u4f20\u5165\uff08\u670d\u52a1\u5185\u8c03\u7528\uff09\nrun_once('gemini25_single_chat')\n```\n\n### \u547d\u4ee4\u884c\uff08CLI\uff09\u7528\u6cd5\n\u65e0\u9700\u6539\u4ee3\u7801\uff0c\u76f4\u63a5\u8fd0\u884c\u4e00\u6b21\uff1a\n```bash\npython -m backend.ai_runner_hjy --config-key gemini25_single_chat\n# \u53ef\u9009\u53c2\u6570\npython -m backend.ai_runner_hjy --config-key gemini25_single_chat --timeout 30 --max-retries 2\n```\n\n### CRUD \u6700\u5c0f\u7528\u6cd5\uff08\u793a\u4f8b\uff09\n```python\nfrom backend.ai_runner_hjy.core.crud import add_param_profile_if_absent, get_param_profile_by_name\n\n# \u5e42\u7b49\u65b0\u589e\uff08\u5b58\u5728\u5219\u8fd4\u56de existed=True\uff09\nres, err = add_param_profile_if_absent(\"default_json\", {\"TEMPERATURE\": 0.2, \"MAX_TOKENS\": 256})\nif err:\n # err \u4e3a (code, message)\uff0c\u4f8b\u5982 (\"DB_ERROR\", \"...\") / (\"VALIDATION_ERROR\", \"...\")\n raise RuntimeError(err)\nprint(res) # {\"id\": 123, \"existed\": False}\n\n# \u67e5\u8be2\nrow, err = get_param_profile_by_name(\"default_json\")\nif err:\n raise RuntimeError(err)\nprint(row[\"params_json\"]) # JSON \u5b57\u7b26\u4e32\n```\n\n---\n\n## \u65e5\u5fd7\u4e0e\u9519\u8bef\u5904\u7406\n- \u6bcf\u6b21\u8c03\u7528\u5fc5\u5199\u4e00\u884c `ai_call_logs`\uff1a\n - \u6210\u529f\uff1a`http_status=200/201`\u3001`duration_ms`\u3001`usage.*`\u3001`response_id`\uff0c\u4ee5\u53ca\u4e24\u5217 JSON\uff08\u82e5\u5b58\u5728\uff09\n - \u5931\u8d25\uff1a`error_code` \u4e3a `HTTP_STATUS_ERROR`\uff08\u975e 2xx\uff09\u6216 `REQUEST_ERROR`\uff08\u7f51\u7edc/\u5e8f\u5217\u5316\uff09\uff0c\u540c\u65f6\u5c3d\u91cf\u4fdd\u7559\u54cd\u5e94 JSON \u4fbf\u4e8e\u6392\u969c\n- \u5efa\u8bae\u5b9a\u671f\u5ba1\u8ba1\uff1a\u6162\u8bf7\u6c42\u3001\u975e 2xx \u6bd4\u4f8b\u3001token \u6d88\u8017\n\n---\n\n## \u5e38\u89c1\u95ee\u9898\uff08FAQ\uff09\n- Q\uff1a\u4e3a\u4ec0\u4e48\u6ca1\u6709\u5199\u5165 `request_body_json/response_body_json`\uff1f\n - A\uff1a\u8bf7\u786e\u8ba4\u4f60\u5df2\u5728 `ai_call_logs` \u65b0\u589e\u4e24\u5217\uff1b\u5426\u5219\u6a21\u5757\u4f1a\u56de\u9000\u5230\u6700\u5c0f\u65e5\u5fd7\u6a21\u5f0f\uff08\u4e0d\u62a5\u9519\uff09\u3002\n- Q\uff1a\u5982\u4f55\u5f3a\u5236\u53ea\u8f93\u51fa JSON\uff1f\n - A\uff1a\u5728 Prompt \u7684 `response_format_json` \u6307\u5b9a `{type:\"json_object\"}`\uff0c\u6216\u5728\u53c2\u6570\u767d\u540d\u5355\u8bbe\u7f6e `JSON_SCHEMA_ENFORCE=true`\uff08\u515c\u5e95\uff09\u3002\n- Q\uff1aAPI Key \u653e\u54ea\u91cc\uff1f\n - A\uff1a\u53ea\u4ee5\u5bc6\u6587 JSON \u5b58\u5728 `ai_connection.api_key_encrypted`\u3002\u8fd0\u884c\u65f6\u7528\u73af\u5883\u53d8\u91cf `AI_PEPPER` \u89e3\u5bc6\uff0c\u660e\u6587\u4e0d\u4f1a\u843d\u5e93/\u65e5\u5fd7\u3002\n\n---\n\n## \u4e0e\u670d\u52a1\u96c6\u6210\uff08\u793a\u4f8b\uff09\n```python\nfrom backend.ai_runner_hjy import run_once\n\ndef handle_request(config_key: str):\n # \u2026 \u4e1a\u52a1\u903b\u8f91\n run_once(config_key)\n```\n> \u82e5\u4f60\u4ee5\u524d\u901a\u8fc7 `subprocess` \u8c03\u811a\u672c\uff0c\u73b0\u5728\u53ef\u4ee5\u76f4\u63a5\u5bfc\u5165\u51fd\u6570\u8c03\u7528\uff0c\u4fbf\u4e8e\u5355\u6d4b\u4e0e\u590d\u7528\u3002\n\n---\n\n## \u8bb8\u53ef\u8bc1\u4e0e\u590d\u7528\n- \u5185\u90e8\u811a\u624b\u67b6\uff0c\u4f9d\u8d56\u6700\u5c0f\u3001\u63a5\u53e3\u6e05\u6670\uff0c\u4fbf\u4e8e\u5728\u4e0d\u540c\u9879\u76ee\u8fc1\u79fb\u590d\u7528\u3002\u6b22\u8fce\u5728\u6b64\u57fa\u7840\u4e0a\u6269\u5c55\uff1a\n - \u589e\u52a0\u7f13\u5b58/\u91cd\u8bd5/\u964d\u7ea7\u7b56\u7565\u53c2\u6570\n - \u5c06\u8bf7\u6c42/\u54cd\u5e94\u843d OSS\uff08\u5927\u4f53\u91cf\uff09\u5e76\u5728\u65e5\u5fd7\u91cc\u4fdd\u5b58\u94fe\u63a5\n - \u63a5\u5165\u66f4\u5b8c\u6574\u7684\u89c2\u6d4b\u4e0e\u6307\u6807\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "DB-config driven OpenAI-compatible caller with auditing (RDS logs), CLI + import API",
"version": "0.0.1",
"project_urls": {
"Homepage": "https://example.com"
},
"split_keywords": [
"openai",
" chat",
" audit",
" mysql",
" langgraph"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "8d234ded51b5d967af7274f8eeddf7076d9228940a4eee025eaa97fa728ef679",
"md5": "0e76bcf9175d052164f94d99a2628088",
"sha256": "9e1d1d62c4dd0b916c53b4ea7e916df0f8c371f29de851eaa5abfaec10f281c9"
},
"downloads": -1,
"filename": "ai_runner_hjy-0.0.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "0e76bcf9175d052164f94d99a2628088",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 21066,
"upload_time": "2025-08-09T12:20:21",
"upload_time_iso_8601": "2025-08-09T12:20:21.736552Z",
"url": "https://files.pythonhosted.org/packages/8d/23/4ded51b5d967af7274f8eeddf7076d9228940a4eee025eaa97fa728ef679/ai_runner_hjy-0.0.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "7ad251bd7935521bf83402de5c30bfd805a1ce32d412a78946e0e3284bd142a4",
"md5": "15a119217b15e74a803c02dcf24c5b66",
"sha256": "58a97f62d1fc3bc1a6875b0e38e4545deeb4a5bdc6266ae49d625e56e6ddc381"
},
"downloads": -1,
"filename": "ai_runner_hjy-0.0.1.tar.gz",
"has_sig": false,
"md5_digest": "15a119217b15e74a803c02dcf24c5b66",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 18313,
"upload_time": "2025-08-09T12:20:23",
"upload_time_iso_8601": "2025-08-09T12:20:23.437700Z",
"url": "https://files.pythonhosted.org/packages/7a/d2/51bd7935521bf83402de5c30bfd805a1ce32d412a78946e0e3284bd142a4/ai_runner_hjy-0.0.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-09 12:20:23",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "ai-runner-hjy"
}