Files
QuoteForge/bible-local/10-agent-api-guide.md
Michael Chus 184f54b663 refactor: привести кодовую базу в соответствие с канонами bible
- 400 → 422 для всех ошибок валидации входных данных (handlers: export, quote, sync, vendor_spec, partnumber_books, pricelist)
- SQL-запросы вынесены из handlers в localdb (partnumber_books, pricelist, support_bundle); ValidateMariaDBConnection перенесён в internal/db/validate.go
- List-ответы унифицированы: ключ items, поля total_count/page/per_page/total_pages (component, pricelist, partnumber_books); шаблоны обновлены
- Молчаливые ошибки заменены на slog.Warn/Error (support_bundle, vendor_spec, component, configuration, local_configuration, localdb)
- N+1 запросы устранены: batch-запросы в export.go и vendor_workspace_import.go
- fmt.Println → slog в cmd/ (qfs, migrate, migrate_ops_projects, migrate_project_updated_at)
- Заголовки recovery/verify добавлены во все 28 SQL-миграций
- Добавлены bible-local/runtime-flows.md и bible-local/decisions/
- Обновлён субмодуль bible до v0.2.0-13

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:38:01 +03:00

17 KiB
Raw Permalink Blame History

10 - Agent API Guide: Pricing Servers from a TZ

This guide is written for an AI agent that needs to price a server configuration (техническое задание, ТЗ) using the QuoteForge HTTP API.

Runtime assumptions

  • QuoteForge runs locally, binds to 127.0.0.1:8080 by default.
  • No authentication is required — the app is single-user, loopback-only.
  • All responses are JSON. All request bodies are JSON unless stated otherwise.
  • The port can be overridden with the QF_SERVER_PORT environment variable.

Base URL for all examples: http://127.0.0.1:8080


Configuration composition rules

These rules are mandatory and must be respected before saving any configuration.

1. Every configuration must belong to a project

Configurations cannot be created in isolation. The correct sequence is:

  1. Create a project (POST /api/projects) and save the returned uuid.
  2. Create the configuration inside that project by passing project_uuid in the config body, or by using POST /api/projects/:uuid/configs.

If the project for a given TZ already exists, retrieve its uuid first:

GET /api/projects?page=1&per_page=100

then pass the matching uuid in project_uuid.

2. Every server configuration must contain all four required component groups

A configuration is not valid for pricing unless items from all four of the following category groups are present:

Category code Meaning Notes
MB Motherboard exactly one MB per configuration
CPU Processor one or more CPUs
MEM Memory / RAM one or more memory modules
PS / PSU Power supply PSU is the current code; PS is legacy — both are accepted

Before saving, verify the assembled BOM with POST /api/quote/validate: the response errors array will contain "Component not found: …" entries for unknown lot names, and warnings will list lots without a price. Reject the configuration and report back to the user if any of the four required categories is missing.

3. Category codes to use when searching

Use category=<code> in GET /api/components to narrow results:

GET /api/components?category=MB&search=X13&has_price=true
GET /api/components?category=CPU&search=Xeon+Gold&has_price=true
GET /api/components?category=MEM&search=32GB+DDR5&has_price=true
GET /api/components?category=PSU&search=800W&has_price=true

Retrieve the full list of active categories at any time:

GET /api/categories

Typical workflow for pricing a server

1.  Check the app is up                 GET  /api/ping
2.  Find or create a project            GET  /api/projects  →  POST /api/projects
3.  Find the latest pricelist           GET  /api/pricelists/latest?source=estimate
4.  Look up lot names for MB            GET  /api/components?category=MB&search=…
5.  Look up lot names for CPU           GET  /api/components?category=CPU&search=…
6.  Look up lot names for MEM           GET  /api/components?category=MEM&search=…
7.  Look up lot names for PSU           GET  /api/components?category=PSU&search=…
8.  (Repeat for other components)       GET  /api/components?category=…&search=…
9.  Validate and calculate the quote    POST /api/quote/validate
10. (Optional) Compare price tiers      POST /api/quote/price-levels
11. Save configuration in the project   POST /api/projects/:uuid/configs

Step 1 — Verify the app is running

GET /api/ping

Response 200 OK:

{"status": "ok"}

Step 2 — Find or create a project

Each TZ maps to one project. Use the TZ identifier as the code field.

Find an existing project

GET /api/projects?page=1&per_page=100

Response 200 OK:

{
  "projects": [
    {
      "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "code": "TZ-123",
      "variant": "",
      "name": "Проект по ТЗ №123",
      "tracker_url": "",
      "is_active": true,
      "created_at": "2026-06-01T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 100
}

Create a new project

POST /api/projects
Content-Type: application/json

Request body:

{
  "code": "TZ-123",
  "name": "Проект по ТЗ №123",
  "tracker_url": ""
}

Fields:

field type required description
code string yes short identifier, unique per variant; use the TZ number or ticket
variant string no variant label within the same code; default is empty string
name string no human-readable title
tracker_url string no link to a ticket or issue tracker

Response 201 Created:

{
  "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "code": "TZ-123",
  "variant": "",
  "name": "Проект по ТЗ №123",
  "is_active": true,
  "created_at": "2026-06-11T10:00:00Z"
}

Save the uuid — it is required to create configurations inside this project.


Step 3 — Find the latest pricelist

QuoteForge maintains three pricing tiers. The source values are:

source meaning
estimate list / catalogue price
warehouse stock price (purchase cost)
competitor competitor reference price
GET /api/pricelists/latest?source=estimate

Response 200 OK:

{
  "id": 42,
  "source": "estimate",
  "version": "2026-05-28",
  "item_count": 12500,
  "is_active": true,
  "created_at": "2026-05-28T06:00:00Z"
}

The id field is a numeric pricelist identifier. Pass it as pricelist_id when calculating a quote to pin pricing to a specific pricelist.

To list all available pricelists:

GET /api/pricelists?source=estimate&active_only=true

Steps 48 — Look up component lot names

Each component is identified by a lot_name (internal SKU). The TZ typically contains model names or descriptions; use the search endpoint to resolve them.

GET /api/components?search=Xeon+Gold+6342&category=CPU&has_price=true&page=1&per_page=20

Query parameters:

parameter default description
search free-text search in lot name and description
category filter by category code (MB, CPU, MEM, PSU, …)
has_price false return only components that have a price
include_hidden false include hidden/retired components
page 1 page number
per_page 20 page size

Response 200 OK:

{
  "components": [
    {
      "lot_name": "CPU-XEON-6342",
      "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz, LGA4189",
      "category": "CPU",
      "category_name": "CPU",
      "model": "Xeon Gold 6342"
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 20
}

To look up a single component by exact lot name:

GET /api/components/CPU-XEON-6342

To list all known categories:

GET /api/categories

Step 9 — Validate and calculate the quote

Before saving, validate the assembled BOM. This catches unknown lot names and missing prices, and also confirms that all required categories are covered.

POST /api/quote/validate
Content-Type: application/json

Request body:

{
  "items": [
    {"lot_name": "MB-X13DAI-N",        "quantity": 1},
    {"lot_name": "CPU-XEON-6342",       "quantity": 2},
    {"lot_name": "RAM-32GB-DDR4-3200",  "quantity": 8},
    {"lot_name": "SSD-480GB-SATA",      "quantity": 2},
    {"lot_name": "PSU-800W-TITANIUM",   "quantity": 2}
  ],
  "pricelist_id": 42
}

Response 200 OK:

{
  "valid": true,
  "items": [
    {
      "lot_name": "MB-X13DAI-N",
      "quantity": 1,
      "unit_price": 95000.00,
      "total_price": 95000.00,
      "description": "Supermicro X13DAi-N dual-socket server board",
      "category": "MB",
      "has_price": true
    },
    {
      "lot_name": "CPU-XEON-6342",
      "quantity": 2,
      "unit_price": 87500.00,
      "total_price": 175000.00,
      "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz",
      "category": "CPU",
      "has_price": true
    },
    {
      "lot_name": "RAM-32GB-DDR4-3200",
      "quantity": 8,
      "unit_price": 12000.00,
      "total_price": 96000.00,
      "description": "32 GB DDR4-3200 ECC RDIMM",
      "category": "MEM",
      "has_price": true
    },
    {
      "lot_name": "PSU-800W-TITANIUM",
      "quantity": 2,
      "unit_price": 18500.00,
      "total_price": 37000.00,
      "description": "800W 80+ Titanium redundant PSU",
      "category": "PSU",
      "has_price": true
    }
  ],
  "errors": [],
  "warnings": [],
  "total": 403000.00
}

Agent check after validation:

  1. valid must be true — all lot names resolved.
  2. errors must be empty — no unknown components.
  3. The returned items array must contain at least one entry from each required category: MB, CPU, MEM, and PS or PSU.
  4. Items with has_price: false are allowed but should be flagged to the user.

If any check fails, do not save the configuration. Report the issue and ask the user to clarify or replace the problematic component.

For simple price totals without validation metadata use POST /api/quote/calculate — identical request body, response contains only items and total.


Step 10 (optional) — Compare price tiers

To see estimate, warehouse, and competitor prices side-by-side for a BOM:

POST /api/quote/price-levels
Content-Type: application/json

Request body:

{
  "items": [
    {"lot_name": "CPU-XEON-6342", "quantity": 2},
    {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8}
  ],
  "pricelist_ids": {
    "estimate": 42,
    "warehouse": 31,
    "competitor": 15
  },
  "no_cache": false
}

pricelist_ids is optional. When omitted the latest pricelist for each source is used automatically.

Response 200 OK:

{
  "items": [
    {
      "lot_name": "CPU-XEON-6342",
      "quantity": 2,
      "estimate_price": 87500.00,
      "warehouse_price": 71000.00,
      "competitor_price": 85000.00,
      "delta_wh_estimate_abs": -16500.00,
      "delta_wh_estimate_pct": -18.86,
      "delta_comp_estimate_abs": -2500.00,
      "delta_comp_estimate_pct": -2.86,
      "delta_comp_wh_abs": 14000.00,
      "delta_comp_wh_pct": 19.72,
      "price_missing": []
    }
  ],
  "resolved_pricelist_ids": {
    "estimate": 42,
    "warehouse": 31,
    "competitor": 15
  }
}

price_missing lists the source names for which no price was found for that lot. Delta fields are null when either operand price is missing.


Step 11 — Save a configuration inside the project

Use the project-scoped endpoint so the configuration is immediately linked to the correct project without a separate move operation.

POST /api/projects/:project_uuid/configs
Content-Type: application/json

The request body is identical to POST /api/configs — the project_uuid field in the body is ignored when using the project-scoped route; the URL parameter takes precedence.

Request body:

{
  "name": "Сервер по ТЗ №123 — вариант А",
  "items": [
    {"lot_name": "MB-X13DAI-N",       "quantity": 1, "unit_price": 95000.00},
    {"lot_name": "CPU-XEON-6342",      "quantity": 2, "unit_price": 87500.00},
    {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8, "unit_price": 12000.00},
    {"lot_name": "SSD-480GB-SATA",     "quantity": 2, "unit_price": 8500.00},
    {"lot_name": "PSU-800W-TITANIUM",  "quantity": 2, "unit_price": 18500.00}
  ],
  "server_model": "2U",
  "support_code": "NBD",
  "server_count": 1,
  "pricelist_id": 42,
  "warehouse_pricelist_id": 31,
  "competitor_pricelist_id": 15,
  "config_type": "server",
  "notes": "Автоматически создано агентом на основании ТЗ №123"
}

Key fields:

field type required description
name string yes human-readable name
items array yes {lot_name, quantity, unit_price} from validate
server_model string no chassis/form-factor code; used for article generation
support_code string no support tier code; used for article generation
server_count int no number of identical servers; total is multiplied
pricelist_id uint no estimate pricelist to attach
warehouse_pricelist_id uint no warehouse pricelist to attach
competitor_pricelist_id uint no competitor pricelist to attach
config_type string no "server" (default) or "storage"
notes string no free text
custom_price float no override total price
disable_price_refresh bool no prevent automatic price refresh on open
only_in_stock bool no filter to in-stock components only

Response 201 Created:

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Сервер по ТЗ №123 — вариант А",
  "items": [...],
  "total_price": 403000.00,
  "server_count": 1,
  "config_type": "server",
  "article": "2U-6342x2-32GBx8-NBD",
  "project_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "created_at": "2026-06-11T10:00:00Z"
}

The uuid can be used for all subsequent operations on this configuration.


Working with saved configurations

GET  /api/configs/:uuid                 — retrieve a saved configuration
PUT  /api/configs/:uuid                 — full update (same body as create)
POST /api/configs/:uuid/refresh-prices  — re-price from latest pricelist
POST /api/configs/:uuid/clone           — duplicate: body {"name": "clone name"}
GET  /api/configs/:uuid/versions        — revision history
GET  /api/configs?page=1&per_page=20    — list all configurations

Error responses

All error responses follow the same shape:

{"error": "human-readable message"}

Common status codes:

code meaning
400 invalid request body or validation failure
404 entity (component, pricelist, config) not found
423 sync readiness is blocked; retry after sync completes
500 internal server error

Minimal end-to-end example

BASE=http://127.0.0.1:8080

# 1. Verify the app is up
curl -s $BASE/api/ping

# 2. Create a project for this TZ
PROJECT_UUID=$(curl -s -X POST $BASE/api/projects \
  -H "Content-Type: application/json" \
  -d '{"code": "TZ-123", "name": "Проект по ТЗ №123"}' | jq -r .uuid)

# 3. Get latest estimate pricelist
PRICELIST_ID=$(curl -s "$BASE/api/pricelists/latest?source=estimate" | jq .id)

# 4. Find lot names for required categories
curl -s "$BASE/api/components?category=MB&search=X13&has_price=true"  | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=CPU&search=Xeon&has_price=true" | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=MEM&search=32GB&has_price=true" | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=PSU&search=800W&has_price=true" | jq '.components[].lot_name'

# 5. Validate the BOM (must contain MB, CPU, MEM, PSU/PS)
curl -s -X POST $BASE/api/quote/validate \
  -H "Content-Type: application/json" \
  -d "{
    \"pricelist_id\": $PRICELIST_ID,
    \"items\": [
      {\"lot_name\": \"MB-X13DAI-N\",        \"quantity\": 1},
      {\"lot_name\": \"CPU-XEON-6342\",       \"quantity\": 2},
      {\"lot_name\": \"RAM-32GB-DDR4-3200\",  \"quantity\": 8},
      {\"lot_name\": \"PSU-800W-TITANIUM\",   \"quantity\": 2}
    ]
  }" | jq '{valid, errors, warnings, total}'

# 6. Save the configuration inside the project
curl -s -X POST "$BASE/api/projects/$PROJECT_UUID/configs" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Сервер по ТЗ №123\",
    \"pricelist_id\": $PRICELIST_ID,
    \"server_model\": \"2U\",
    \"server_count\": 1,
    \"config_type\": \"server\",
    \"items\": [
      {\"lot_name\": \"MB-X13DAI-N\",        \"quantity\": 1,  \"unit_price\": 95000},
      {\"lot_name\": \"CPU-XEON-6342\",       \"quantity\": 2,  \"unit_price\": 87500},
      {\"lot_name\": \"RAM-32GB-DDR4-3200\",  \"quantity\": 8,  \"unit_price\": 12000},
      {\"lot_name\": \"PSU-800W-TITANIUM\",   \"quantity\": 2,  \"unit_price\": 18500}
    ]
  }" | jq '{uuid, total_price, article}'