# dotpromptz-py

`dotpromptz-py` 是一个可执行 `.prompt` 模板格式的 Python 实现，支持：

- Jinja2 模板渲染
- 多模型适配器（`openai` / `anthropic` / `google`）
- 批量输入执行
- 结构化输出（`json` / `yaml` / `txt` / `image`）

## CLI 用法

安装 CLI（推荐，使用 `uv tool`）：

```bash
uv tool install dotpromptz-py
uv tool upgrade dotpromptz-py
```

如果你在仓库内开发，再执行：

```bash
uv sync
```

查看帮助：

```bash
prompt --help
```

### 全局参数

```bash
prompt --version
prompt --log-level DEBUG run demo.prompt
prompt --upgrade
```

- `--log-level`: `DEBUG|INFO|WARNING|ERROR|CRITICAL`，默认 `INFO`
- `--upgrade` / `-U`: 通过 `uv` 升级 `dotpromptz-py`

### 1) 运行 `.prompt`（会调用模型）

```bash
prompt run path/to/demo.prompt
prompt run path/to/demo.prompt --force
```

- `run` 会执行完整流程：编译、渲染、调用模型、写出结果文件
- 当输出文件已存在时，需加 `--force`
- `run` 前置要求：`.prompt` 里必须配置 `output.format` 和 `output.file_name`

### 2) 构建 `.prompt`（不调用模型）

```bash
prompt build path/to/demo.prompt
```

- `build` 只渲染，不请求 LLM
- 会在日志目录生成一个 `build_<prompt名>_<时间>_<pid>.yaml`，用于检查渲染后的消息与配置

### 3) 合并输入目录

```bash
prompt merge ./data
prompt merge ./data --pattern "**/*.json" -W --force
```

- 支持输入扩展名：`.json` `.yaml` `.yml` `.txt`
- `--pattern`: 自定义 glob 匹配
- `-W` / `--with-filename`: 注入 `FILENAME=<文件名去后缀>`
- 输出文件：
  - 合并 JSON/TXT -> `上级目录/<目录名>.jsonl`
  - 合并 YAML -> `上级目录/<目录名>.yaml`

### 4) Git 自动化命令

```bash
prompt git sync
prompt git push
prompt git submit path/to/demo.prompt
prompt git submit path/to/demo.prompt --no-mail
```

- `git sync`: `fetch + pull --ff-only`
- `git push`: 自动 `git add .`、生成提交信息、提交并推送
- `git submit`: 自动按需安装/更新并触发 `submit-prompt` GitHub Workflow（依赖 `gh auth login`）

## `.prompt` 写法

`.prompt` 文件由两部分组成：

1. Frontmatter（YAML，必须用 `---` 包裹）
2. 模板体（消息内容，支持 Jinja2）

### 1) 最小可用结构

```prompt
---
version: 1
---
:::user
hello
```

约束：

- 第一条非空行必须是 `---`
- frontmatter 必须有结束分隔符 `---`
- `version` 必填，且当前必须为 `1`
- frontmatter 顶层字段只能是白名单字段（见下表），未知字段会报错

### 2) Frontmatter 顶层字段

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `version` | `int` | 是 | `.prompt` 协议版本，当前固定为 `1` |
| `description` | `str` | 否 | 描述信息，可用于日志/元信息 |
| `adapter` | `str` 或 `object` | 否 | 模型提供方适配器配置 |
| `config` | `object` | 否 | 生成参数（模型名、采样参数、图片参数等） |
| `input` | `object` | 否 | 输入数据来源与默认值 |
| `output` | `object` | 否 | 输出格式与文件命名（`run` 必需） |
| `runtime` | `object` | 否 | 并发、超时、重试等运行参数 |

### 3) `adapter` 参数

`adapter` 支持两种写法：

```yaml
adapter: openai
```

```yaml
adapter:
  name: openai
  group: azure-east
```

```yaml
adapter:
  name: openai
  groups: [azure-east, azure-west]
```

参数说明：

- `name`: 适配器名称（如 `openai` / `anthropic` / `google`）
- `group`: 指定单个凭证分组
- `groups`: 指定多个分组进行轮换
- 如果 `group` 和 `groups` 同时存在，运行时会优先按 `group` 过滤

### 4) `config` 参数（模型生成参数）

`config` 会严格校验字段名，支持参数如下：

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `model` | `str` | 模型名称；未配置时使用适配器默认模型 |
| `thinking` | `low \| medium \| high` | 思考强度级别 |
| `temperature` | `float` | 采样温度 |
| `top_p` | `float` | nucleus sampling 参数 |
| `max_tokens` | `int` | 最大输出 token 数 |
| `stop` | `list[str]` | 停止序列 |
| `frequency_penalty` | `float` | 频率惩罚 |
| `presence_penalty` | `float` | 存在惩罚 |
| `seed` | `int` | 随机种子 |
| `resolution` | `str` | 图片输出分辨率（图片生成时） |
| `aspect_ratio` | `str` | 图片宽高比（图片生成时） |
| `transparent` | `bool` | 透明背景（图片生成时） |

图片相关参数的适配器差异：

- OpenAI
  - `resolution`: `auto` / `1024x1024` / `1536x1024` / `1024x1536`
  - `aspect_ratio`: `1:1` / `3:2` / `2:3`
  - `transparent`: `true/false`
- Google Gemini
  - `resolution`: `1K` / `2K` / `4K`
  - `aspect_ratio`: `1:1` / `2:3` / `3:2` / `3:4` / `4:3` / `9:16` / `16:9` / `21:9`
  - `transparent`: 当前会被忽略（会记录 warning）

### 5) `input` 参数

`input` 结构：

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `chain` | `bool` | 是否由上游链式运行时提供输入源 |
| `defaults` | `dict` | 每条输入都会先注入默认值，后续同名键可覆盖 |
| `data` | `dict` 或 `list[dict]` | 直接内联输入数据 |
| `files` | `str` 或 `list[str]` | 从文件或目录加载输入数据 |

约束：

- `data` 与 `files` 互斥，不能同时配置
- `chain` 与 `data` / `files` 互斥，但可与 `defaults` 一起使用
- `files` 路径支持相对路径（相对于 `.prompt` 文件所在目录）
- `files` 支持扩展名：`.json` / `.yaml` / `.yml` / `.jsonl`
- 当 `files` 指向目录时，仅扫描目录顶层文件（不递归）
- 目录不能为空；目录中出现不支持扩展名会直接报错
- 目录内文件后缀必须完全一致（`.yaml` 与 `.yml` 视为不同后缀）
- 当 `files` 列表中包含目录时，展开后的全部文件也必须后缀一致

示例：

```yaml
input:
  defaults:
    lang: zh
  data:
    topic: AI
```

```yaml
input:
  files:
    - ./data/a.json
    - ./data/b.jsonl
```

```yaml
input:
  files: ./data/users
```

链式输入示例：

```yaml
input:
  chain: true
  defaults:
    role: reviewer
```

### 6) `output` 参数

`run` 命令必须配置 `output`，并且至少包含 `format` 和 `file_name`。

| 参数 | 类型 | 必填（run） | 说明 |
| --- | --- | --- | --- |
| `format` | `json \| yaml \| txt \| image` | 是 | 输出格式；`image` 会输出 `.png` |
| `file_name` | `str` | 是 | 输出文件名模板（不含扩展名） |
| `output_dir` | `str` | 否 | 输出目录模板，默认 `output/{{NAME}}_{{TIME}}` |
| `schema` | `dict` | 否 | JSON Schema（仅 `json/yaml` 有效） |
| `example` | `str` | 否 | YAML 示例字符串（自动推断 schema，且与 `schema` 互斥） |

补充说明：

- `output.schema` 与 `output.example` 互斥
- `output.example` 必须是 YAML 字符串（建议 `|` 块写法）
- 批量运行时如果多个输入渲染出相同的输出路径，会直接报错

`output.schema` 还支持包装写法，可在保留 schema 校验的同时直接指定固定输出值：

```yaml
output:
  format: json
  file_name: result
  schema:
    data:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
      required: [name, age]
      additionalProperties: false
    fixed:
      name: Alice
      age: 18
```

补充说明：

- `schema.data` 是真正参与校验的 JSON Schema
- `schema.fixed` 会跳过模型调用，直接作为输出内容写文件
- `schema.fixed` 仅支持 `json` / `yaml`
- 写出前仍会先按 `schema.data` 校验
- 详细示例见 `docs/fixed.md`

`output.example` 示例：

```yaml
output:
  format: json
  file_name: '{{NAME}}_{{INDEX}}'
  example: |
    summary: 示例摘要
    tags__enum:
      - ai
      - llm
    tags:
      - ai
```

### 7) `runtime` 参数

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `max_workers` | `int` | `5` | 批处理并发数，必须 `>= 1` |
| `timeout` | `float \| null` | `null` | 单次请求超时秒数，`null` 表示不限制 |
| `retry.max_retries` | `int` | `3` | 最大重试次数，必须 `>= 0` |
| `retry.retry_delay` | `float` | `1.0` | 重试基础延迟（秒），线性递增 |
| `next` | `str \| null` | `null` | 当前 item 成功写出后立即触发的下一条 `.prompt` 路径 |

重试延迟计算方式：`retry_delay * (attempt + 1)`，例如 `1.0` 时是 `1s, 2s, 3s...`。

`runtime.next` 按当前 `.prompt` 文件所在目录解析相对路径。链式子 prompt 若声明 `input.chain: true`，则输入源由上游刚写出的输出文件提供。

### 8) 消息块语法（模板体）

支持块：

- `:::system`
- `:::user`
- `:::model`
- `:::image`

示例：

```prompt
---
version: 1
---
:::system
你是一个简洁的助手。

:::user
请介绍 {{ topic }}

:::model
好的，我来介绍。

:::user
先看这张图：
:::image
./assets/cat.png
```

规则：

- 使用块语法时，第一条消息前不能出现普通文本
- 块指令不支持行内参数（例如 `:::user xxx` 是非法）
- `:::image` 必须出现在某个 role 块之后
- `:::image` 的值必须写在后续第一条非空行（URL 或路径）
- 如果模板体完全不写块，则整段文本会被当成一条 `user` 消息

`:::image` 支持：

- `http://` / `https://` URL
- 本地文件路径（相对路径按 `.prompt` 所在目录解析）

不支持：

- `data:` URL（会报错）

### 9) 模板变量（Jinja2）

可以在模板体以及 frontmatter 的字符串字段中使用 `{{ ... }}`（`input` 字段不参与 frontmatter 变量渲染）。

变量来源：

- `input.data` / `input.files` / `input.defaults`
- 自动变量：
  - `TIME`: 时间戳，格式 `YYYYMMDD_HHMMSS`
  - `NAME`: `.prompt` 文件名（不含后缀）
  - `INDEX`: 批处理索引（主要用于 `output.file_name` / `output.output_dir`）
  - `NUMBER`: 基于最终输出文件路径冲突自动递增，不再仅按目录是否已存在判断
  - `SCHEMA`: `output.schema` 的 JSON 字符串
  - `TEXT`: 当 `input.chain: true` 且上游输出是 `.txt` 文件时，注入该文件内容

注意：

- 输入变量名不要使用全大写（会与自动变量命名空间冲突）
- 模板里引用未注册的全大写变量会触发校验错误

### 10) 完整可运行示例（结构化输出）

```prompt
---
version: 1
description: 生成摘要
adapter: openai
config:
  model: gpt-4o-mini
  thinking: low
  temperature: 0.2
  top_p: 0.9
  max_tokens: 800
input:
  data:
    topic: Dotprompt
output:
  format: json
  file_name: '{{NAME}}_{{TIME}}'
  schema:
    type: object
    properties:
      summary:
        type: string
      keywords:
        type: array
        items:
          type: string
    required: [summary, keywords]
    additionalProperties: false
runtime:
  max_workers: 3
  timeout: 30
  retry:
    max_retries: 2
    retry_delay: 1.0
---
:::system
你是一个技术写作助手。

:::user
请用中文总结 {{ topic }}，返回 JSON，必须满足这个 schema：
{{ SCHEMA }}
```

执行：

```bash
prompt run ./summary.prompt
```
