ai-runner-hjy


Nameai-runner-hjy JSON
Version 0.0.1 PyPI version JSON
download
home_pageNone
SummaryDB-config driven OpenAI-compatible caller with auditing (RDS logs), CLI + import API
upload_time2025-08-09 12:20:23
maintainerNone
docs_urlNone
authorhjy
requires_python>=3.9
licenseMIT
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"
}
        
hjy
Elapsed time: 0.87570s