Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
ONEC_AGENT_HOST=0.0.0.0 \
|
||||
ONEC_AGENT_PORT=8090
|
||||
|
||||
WORKDIR /app
|
||||
RUN pip install --no-cache-dir pyyaml==6.0.2
|
||||
COPY scripts /app/scripts
|
||||
COPY plugins/1c /app/plugins/1c
|
||||
COPY config /app/config
|
||||
COPY registry /app/registry
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
CMD ["python", "/app/plugins/1c/agent/agent_server.py", "--host", "0.0.0.0", "--port", "8090"]
|
||||
@@ -0,0 +1,148 @@
|
||||
# 1C Agent (подпроект)
|
||||
|
||||
Этот подпроект — отдельный сервис-агент для 1С с:
|
||||
|
||||
- хранением состояния `project -> chat -> messages`;
|
||||
- маршрутизацией модели через `registry/index.json` + `runtime_profiles`;
|
||||
- вызовом LLM через настраиваемые провайдеры;
|
||||
- встроенной RAG-подготовкой контекста по `plugins/1c/datasets/prepared/rag_index.json`;
|
||||
- вызовами 1C-адаптера (`/onec-tool` / `adapter_calls`).
|
||||
|
||||
## API (коротко)
|
||||
|
||||
- `GET /v1/health` — здоровье сервиса.
|
||||
- `GET /v1/projects` — список проектов.
|
||||
- `POST /v1/projects` — создать проект.
|
||||
- `GET /v1/projects/{project_id}` — получить проект.
|
||||
- `GET /v1/projects/{project_id}/chats` — список чатов проекта.
|
||||
- `POST /v1/projects/{project_id}/chats` — создать чат.
|
||||
- `GET /v1/projects/{project_id}/chats/{chat_id}` — получить чат.
|
||||
- `GET /v1/projects/{project_id}/chats/{chat_id}/messages` — история сообщений.
|
||||
- `POST /v1/projects/{project_id}/chats/{chat_id}/messages` — добавить message (user/assistant/system/tool).
|
||||
- `GET /v1/projects/{project_id}/chats/{chat_id}/runtime` — показать, как в этом чате разрешается запуск модели (provider/model/model_id/base_url/adapter и настройки).
|
||||
- `POST /v1/projects/{project_id}/chats/{chat_id}/turn` — сделать turn LLM:
|
||||
- принимает `message`
|
||||
- может включать `adapter_calls` (список `{method, params}`)
|
||||
- может включать `use_rag` и RAG-профиль.
|
||||
- `POST /v1/projects/{project_id}/chats/{chat_id}/onec-tool` — прямой вызов адаптера.
|
||||
- `PATCH /v1/projects/{project_id}` — обновить проект (название/описание/metadata).
|
||||
- `PATCH /v1/projects/{project_id}/chats/{chat_id}` — обновить настройки чата.
|
||||
- `DELETE /v1/projects/{project_id}` — удалить проект вместе со всеми чатами/сообщениями.
|
||||
- `DELETE /v1/projects/{project_id}/chats/{chat_id}` — удалить чат и его сообщения.
|
||||
- `GET /v1/state` — диагностика состояния сервиса (запущен, uptime, счётчики, провайдеры).
|
||||
|
||||
## Веб-интерфейс
|
||||
|
||||
- Открой `http://<host>:<port>/` (на тесте: `http://docker-test.cin.su:8090/`), чтобы войти в UI.
|
||||
- UI не требует отдельной авторизации в самом сервисе; доступ регулируется только сетевыми правилами/портом.
|
||||
- API по-прежнему доступен на `/v1/*` для интеграций и для работы без UI.
|
||||
|
||||
## Контракт
|
||||
|
||||
Нормализованный контракт API: [openapi.yaml](openapi.yaml).
|
||||
|
||||
## Разделение ответственностей
|
||||
|
||||
### Агент (этот сервис)
|
||||
- Определяет сценарий: project/chat/role/memory/tool-call/guardrails.
|
||||
- Собирает prompt:
|
||||
1) системный prompt проекта/чата,
|
||||
2) историю чата,
|
||||
3) RAG-подготовленный контекст (если включён),
|
||||
4) результаты инструментов.
|
||||
- Вызывает LLM через выбранный провайдер.
|
||||
- Сохраняет сообщение и служебные метаданные.
|
||||
|
||||
### Модель
|
||||
- Принимает стандартный диалоговый список сообщений.
|
||||
- Возвращает только сгенерированный текст.
|
||||
- Никакой бизнес-логики 1С в сервисе модели — только reasoning по prompt.
|
||||
|
||||
### Логика работы с адаптером в prompt
|
||||
|
||||
Правила выбора маршрута поиска живут в `plugins/1c/prompts/system.md`, а не в REST/MCP-адаптере. Адаптер должен возвращать структурированные факты и статусы, а агент обязан правильно их интерпретировать.
|
||||
|
||||
Ключевые правила для агента:
|
||||
- всегда явно передавать `base_id` для live-запросов;
|
||||
- читать известный `module_ref`/read selector напрямую перед глобальным поиском;
|
||||
- не считать `not_found` доказательством отсутствия кода в расширениях или `ConfigCAS`;
|
||||
- считать `partial`, `truncated=true` и timeout неполным результатом;
|
||||
- отделять доказанный факт от гипотезы и показывать конкретный участок кода, если пользователь просит "место".
|
||||
|
||||
### Адаптер (1С)
|
||||
- Отвечает за живые данные 1С (`query`, `metadata`, `modules`, и т.д.).
|
||||
- Возвращает строго структурированный JSON.
|
||||
- Не должен быть «зашит» в модель: модель может запрашивать только через инструментовое API.
|
||||
|
||||
### RAG
|
||||
- Не является моделью.
|
||||
- Превращает вопрос в релевантный контекст + мета-информацию источников.
|
||||
- Передаётся в модель как системное сообщение с уже собранным `rag_prompt`.
|
||||
|
||||
## Что проходит между слоями
|
||||
|
||||
- `agent -> model`:
|
||||
`{"role":"system"/"user"/"assistant", "content": ...}` + опционально `provider/system` настройки.
|
||||
- `agent -> adapter`:
|
||||
`{"method": "...", "payload": {...}}`.
|
||||
- `adapter -> agent`:
|
||||
structured JSON-ответ (метаданные/результаты запроса/ошибки).
|
||||
- `agent -> клиент`:
|
||||
user message + assistant message + RAG-сводка + tool outputs + guardrail info.
|
||||
|
||||
## Быстрый smoke на развернутом сервисе
|
||||
|
||||
Для проверки продового/тестового инстанса:
|
||||
|
||||
- `python scripts/smoke_onec_agent_api.py --base-url http://docker-test.cin.su:8090`
|
||||
- По умолчанию скрипт проверяет быстрые роуты (`/health`, `/state`, CRUD, `messages`).
|
||||
- Для live-проверки `/turn` добавь `--with-turn-check` и учти, что первый запуск может идти дольше из‑за холодного инференса.
|
||||
|
||||
- Быстрый авто-проход "deploy + smoke":
|
||||
- `powershell -File scripts/deploy_onec_agent_test.ps1 -NoBuild`
|
||||
- `powershell -File scripts/deploy_onec_agent_test.ps1`
|
||||
- `powershell -File scripts/deploy_onec_agent_test.ps1 -WithTurnCheck`
|
||||
|
||||
## Обновление на `docker-test` без влияния на остальные проекты
|
||||
|
||||
Деплой скрипт работает только с сервисом `onec-agent`, поэтому на `docker-test` не затрагиваются другие проекты.
|
||||
|
||||
- Быстрое обновление (с пересборкой):
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy_onec_agent_test.ps1`
|
||||
- Обновление только с перезапуском (без сборки):
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy_onec_agent_test.ps1 -NoBuild`
|
||||
- Если добавляешь скрипт в задачу по расписанию, используй именно этот файл — он делает деплой + health-check + smoke в одном проходе.
|
||||
|
||||
## Автообновление при изменении кода
|
||||
|
||||
Если хочешь, чтобы агент всегда был в актуальном состоянии после каждой правки его исходников, запусти:
|
||||
|
||||
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/watch_onec_agent_for_test.ps1`
|
||||
|
||||
Скрипт мониторит каталоги/файлы:
|
||||
- `plugins/1c/agent`
|
||||
- `core/deploy/docker/1c-agent/compose.yaml`
|
||||
- `scripts/deploy_onec_agent_test.ps1`
|
||||
- `scripts/smoke_onec_agent_api.py`
|
||||
|
||||
При любых изменениях автоматически перезапускает только сервис `onec-agent` на `docker-test`, затем делает health-check и smoke.
|
||||
|
||||
Для непрерывного режима в фоне:
|
||||
|
||||
- `Start-Process -FilePath powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','scripts/watch_onec_agent_for_test.ps1' -WindowStyle Hidden`
|
||||
|
||||
## Что хранит агент (сущности)
|
||||
|
||||
- `Project`: `id`, `name`, `description`, `metadata`, `created_at`, `updated_at`.
|
||||
- `Chat`: `id`, `project_id`, `title`, `provider_id`, `base_url`, `served_model_name`, `model_id`, `temperature`, `max_tokens`, `rag_profile`, `rag_limit`, `system_prompt`, `metadata`, `created_at`, `updated_at`.
|
||||
- `Message`: `id`, `project_id`, `chat_id`, `role`, `content`, `payload` (операционные поля), `created_at`.
|
||||
|
||||
## Как подключать другого ИИ в будущем
|
||||
|
||||
Сервис уже рассчитан на расширение:
|
||||
|
||||
- в `ONEC_AGENT_PROVIDERS` задаются провайдеры;
|
||||
- у каждого провайдера есть `type`.
|
||||
- сейчас поддержан `openai-compatible`.
|
||||
|
||||
Когда нужна другая платформа, добавляется новый `type` в диспетчер провайдеров (и, при необходимости, нормализация ответа в единый формат `{"text": "...", "raw": {...}}`).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,779 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 1C Agent API
|
||||
version: 0.1.0
|
||||
description: Independent 1C agent API for project/chat orchestration with model calls, RAG and adapter calls.
|
||||
servers:
|
||||
- url: http://localhost:8090
|
||||
paths:
|
||||
/v1/health:
|
||||
get:
|
||||
operationId: getHealth
|
||||
summary: Service health
|
||||
responses:
|
||||
"200":
|
||||
description: Service is alive.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
/v1/state:
|
||||
get:
|
||||
operationId: getState
|
||||
summary: Service state and counters
|
||||
responses:
|
||||
"200":
|
||||
description: Current internal state.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StateResponse"
|
||||
/v1/projects:
|
||||
get:
|
||||
operationId: listProjects
|
||||
summary: List projects
|
||||
responses:
|
||||
"200":
|
||||
description: Project list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectsListResponse"
|
||||
post:
|
||||
operationId: createProject
|
||||
summary: Create project
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectCreateRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Project created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectResponse"
|
||||
/v1/projects/{project_id}:
|
||||
get:
|
||||
operationId: getProject
|
||||
summary: Get project by id
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
responses:
|
||||
"200":
|
||||
description: Project
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectResponse"
|
||||
patch:
|
||||
operationId: updateProject
|
||||
summary: Update project metadata
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectUpdateRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated project
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProjectResponse"
|
||||
delete:
|
||||
operationId: deleteProject
|
||||
summary: Delete project
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
responses:
|
||||
"204":
|
||||
description: Project deleted
|
||||
/v1/projects/{project_id}/chats:
|
||||
get:
|
||||
operationId: listChats
|
||||
summary: List chats in project
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
responses:
|
||||
"200":
|
||||
description: Chats list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatsListResponse"
|
||||
post:
|
||||
operationId: createChat
|
||||
summary: Create chat in project
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatCreateRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Chat created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatResponse"
|
||||
/v1/projects/{project_id}/chats/{chat_id}:
|
||||
get:
|
||||
operationId: getChat
|
||||
summary: Get chat config
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
responses:
|
||||
"200":
|
||||
description: Chat
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatResponse"
|
||||
patch:
|
||||
operationId: updateChat
|
||||
summary: Update chat config
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatUpdateRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated chat
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatResponse"
|
||||
delete:
|
||||
operationId: deleteChat
|
||||
summary: Delete chat
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
responses:
|
||||
"204":
|
||||
description: Chat deleted
|
||||
/v1/projects/{project_id}/chats/{chat_id}/messages:
|
||||
get:
|
||||
operationId: listMessages
|
||||
summary: List chat messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 200
|
||||
responses:
|
||||
"200":
|
||||
description: Message history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesListResponse"
|
||||
post:
|
||||
operationId: addMessage
|
||||
summary: Add message to chat
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AddMessageRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Message added
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessageResponse"
|
||||
/v1/projects/{project_id}/chats/{chat_id}/runtime:
|
||||
get:
|
||||
operationId: getChatRuntime
|
||||
summary: Resolve runtime context for project/chat
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
responses:
|
||||
"200":
|
||||
description: Runtime snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChatRuntimeResponse"
|
||||
/v1/projects/{project_id}/chats/{chat_id}/turn:
|
||||
post:
|
||||
operationId: chatTurn
|
||||
summary: Make one agent turn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TurnRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Turn result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TurnResponse"
|
||||
/v1/projects/{project_id}/chats/{chat_id}/onec-tool:
|
||||
post:
|
||||
operationId: callOneCTool
|
||||
summary: Call 1C adapter tool
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ProjectId"
|
||||
- $ref: "#/components/parameters/ChatId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ToolCallRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Tool result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ToolCallResponse"
|
||||
/v1/models:
|
||||
get:
|
||||
operationId: listModels
|
||||
summary: List candidate models from registry
|
||||
responses:
|
||||
"200":
|
||||
description: Models list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ModelsResponse"
|
||||
/v1/providers:
|
||||
get:
|
||||
operationId: listProviders
|
||||
summary: List provider registry
|
||||
responses:
|
||||
"200":
|
||||
description: Providers list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProvidersResponse"
|
||||
components:
|
||||
parameters:
|
||||
ProjectId:
|
||||
name: project_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
ChatId:
|
||||
name: chat_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
schemas:
|
||||
TraceEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
trace_id:
|
||||
type: string
|
||||
required:
|
||||
- trace_id
|
||||
ErrorPayload:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [error]
|
||||
error:
|
||||
type: object
|
||||
required:
|
||||
- message
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
required: [status, error]
|
||||
Project:
|
||||
type: object
|
||||
required: [id, name, description, metadata, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
created_at:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
Chat:
|
||||
type: object
|
||||
required: [id, project_id, title, provider_id, temperature, max_tokens, rag_profile, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
project_id:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
model_id:
|
||||
type: string
|
||||
nullable: true
|
||||
provider_id:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
nullable: true
|
||||
served_model_name:
|
||||
type: string
|
||||
nullable: true
|
||||
temperature:
|
||||
type: number
|
||||
max_tokens:
|
||||
type: integer
|
||||
rag_profile:
|
||||
type: string
|
||||
rag_limit:
|
||||
type: integer
|
||||
nullable: true
|
||||
system_prompt:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
created_at:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
Message:
|
||||
type: object
|
||||
required: [id, project_id, chat_id, role, content, payload, created_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
project_id:
|
||||
type: string
|
||||
chat_id:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
enum: [user, assistant, system, tool]
|
||||
content:
|
||||
type: string
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
created_at:
|
||||
type: string
|
||||
ProjectCreateRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ProjectUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ChatCreateRequest:
|
||||
type: object
|
||||
required: [title]
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
model_id:
|
||||
type: string
|
||||
provider_id:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
served_model_name:
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
max_tokens:
|
||||
type: integer
|
||||
rag_profile:
|
||||
type: string
|
||||
rag_limit:
|
||||
type: integer
|
||||
system_prompt:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ChatUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
model_id:
|
||||
type: string
|
||||
provider_id:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
served_model_name:
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
max_tokens:
|
||||
type: integer
|
||||
rag_profile:
|
||||
type: string
|
||||
rag_limit:
|
||||
type: integer
|
||||
system_prompt:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
AddMessageRequest:
|
||||
type: object
|
||||
required: [role, content]
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
enum: [user, assistant, system, tool]
|
||||
content:
|
||||
type: string
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ToolCallRequest:
|
||||
type: object
|
||||
required: [method]
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
params:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
TurnRequest:
|
||||
type: object
|
||||
required: [message]
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
use_rag:
|
||||
type: boolean
|
||||
default: true
|
||||
rag_profile:
|
||||
type: string
|
||||
rag_limit:
|
||||
type: integer
|
||||
provider_id:
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
max_tokens:
|
||||
type: integer
|
||||
history_limit:
|
||||
type: integer
|
||||
minimum: 1
|
||||
adapter_calls:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [method]
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
params:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
served_model_name:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
HealthResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
service:
|
||||
type: string
|
||||
time:
|
||||
type: string
|
||||
required: [status, service, time]
|
||||
StateResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
required:
|
||||
- service
|
||||
- started_at
|
||||
- uptime_seconds
|
||||
- state
|
||||
- providers
|
||||
- paths
|
||||
properties:
|
||||
service:
|
||||
type: string
|
||||
started_at:
|
||||
type: string
|
||||
uptime_seconds:
|
||||
type: number
|
||||
state:
|
||||
type: object
|
||||
required: [projects, chats, messages]
|
||||
properties:
|
||||
projects:
|
||||
type: integer
|
||||
chats:
|
||||
type: integer
|
||||
messages:
|
||||
type: integer
|
||||
providers:
|
||||
type: object
|
||||
required: [count, ids]
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
paths:
|
||||
type: object
|
||||
required: [adapter_url, database]
|
||||
properties:
|
||||
adapter_url:
|
||||
type: string
|
||||
nullable: true
|
||||
database:
|
||||
type: string
|
||||
ProjectsListResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
projects:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Project"
|
||||
ProjectResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
project:
|
||||
$ref: "#/components/schemas/Project"
|
||||
ChatsListResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
project_id:
|
||||
type: string
|
||||
chats:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Chat"
|
||||
ChatResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
chat:
|
||||
$ref: "#/components/schemas/Chat"
|
||||
routing:
|
||||
type: object
|
||||
nullable: true
|
||||
MessageResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
message:
|
||||
$ref: "#/components/schemas/Message"
|
||||
MessagesListResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
project_id:
|
||||
type: string
|
||||
chat_id:
|
||||
type: string
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Message"
|
||||
ChatRuntimeResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
project_id:
|
||||
type: string
|
||||
chat_id:
|
||||
type: string
|
||||
runtime:
|
||||
type: object
|
||||
required:
|
||||
- project
|
||||
- chat
|
||||
- provider
|
||||
- model
|
||||
- runtime_settings
|
||||
- adapter
|
||||
properties:
|
||||
project:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
chat:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
provider:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
model:
|
||||
type: object
|
||||
properties:
|
||||
chat_model_id:
|
||||
type: string
|
||||
nullable: true
|
||||
selected_model_id:
|
||||
type: string
|
||||
served_model:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
route:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
runtime_settings:
|
||||
type: object
|
||||
properties:
|
||||
temperature:
|
||||
type: number
|
||||
max_tokens:
|
||||
type: integer
|
||||
rag_profile:
|
||||
type: string
|
||||
rag_limit:
|
||||
type: integer
|
||||
nullable: true
|
||||
system_prompt:
|
||||
type: string
|
||||
adapter:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
nullable: true
|
||||
TurnResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
project_id:
|
||||
type: string
|
||||
chat_id:
|
||||
type: string
|
||||
user_message:
|
||||
$ref: "#/components/schemas/Message"
|
||||
assistant_message:
|
||||
$ref: "#/components/schemas/Message"
|
||||
rag:
|
||||
type: object
|
||||
nullable: true
|
||||
tools:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
nullable: true
|
||||
guardrail:
|
||||
type: object
|
||||
nullable: true
|
||||
ToolCallResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
tool_result:
|
||||
type: object
|
||||
message:
|
||||
$ref: "#/components/schemas/Message"
|
||||
ModelsResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
ProvidersResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TraceEnvelope"
|
||||
- type: object
|
||||
properties:
|
||||
providers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
@@ -0,0 +1,566 @@
|
||||
const els = {
|
||||
apiBase: document.getElementById("apiBase"),
|
||||
healthStatus: document.getElementById("healthStatus"),
|
||||
projectSelect: document.getElementById("projectSelect"),
|
||||
chatSelect: document.getElementById("chatSelect"),
|
||||
messages: document.getElementById("messages"),
|
||||
messageInput: document.getElementById("messageInput"),
|
||||
chatForm: document.getElementById("chatForm"),
|
||||
sendButton: document.getElementById("sendButton"),
|
||||
createProjectBtn: document.getElementById("createProject"),
|
||||
refreshProjectsBtn: document.getElementById("refreshProjects"),
|
||||
createChatBtn: document.getElementById("createChat"),
|
||||
renameProjectBtn: document.getElementById("renameProject"),
|
||||
deleteProjectBtn: document.getElementById("deleteProject"),
|
||||
renameChatBtn: document.getElementById("renameChat"),
|
||||
clearMessagesBtn: document.getElementById("clearMessages"),
|
||||
clearChatMemoryBtn: document.getElementById("clearChatMemory"),
|
||||
checkHealthBtn: document.getElementById("checkHealth"),
|
||||
projectNameInput: document.getElementById("projectName"),
|
||||
chatNameInput: document.getElementById("chatName"),
|
||||
};
|
||||
|
||||
const PROJECT_ID_KEY = "onecAgent.projectId";
|
||||
const CHAT_BY_PROJECT_KEY = "onecAgent.chatByProject";
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 120000;
|
||||
const TURN_TIMEOUT_MS = 90000;
|
||||
|
||||
const state = {
|
||||
projectId: "",
|
||||
chatId: "",
|
||||
chatByProject: {},
|
||||
};
|
||||
|
||||
function loadStateFromStorage() {
|
||||
try {
|
||||
const projectId = localStorage.getItem(PROJECT_ID_KEY);
|
||||
if (projectId) {
|
||||
state.projectId = projectId;
|
||||
}
|
||||
const map = localStorage.getItem(CHAT_BY_PROJECT_KEY);
|
||||
if (map) {
|
||||
const parsed = JSON.parse(map);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
state.chatByProject = parsed;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
state.projectId = "";
|
||||
state.chatByProject = {};
|
||||
}
|
||||
}
|
||||
|
||||
function persistProjectId(projectId) {
|
||||
state.projectId = projectId || "";
|
||||
try {
|
||||
if (projectId) {
|
||||
localStorage.setItem(PROJECT_ID_KEY, projectId);
|
||||
} else {
|
||||
localStorage.removeItem(PROJECT_ID_KEY);
|
||||
}
|
||||
} catch {
|
||||
// no-op if localStorage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function persistChatId(projectId, chatId) {
|
||||
const map = state.chatByProject && typeof state.chatByProject === "object" ? { ...state.chatByProject } : {};
|
||||
if (projectId && chatId) {
|
||||
map[projectId] = chatId;
|
||||
} else if (projectId) {
|
||||
delete map[projectId];
|
||||
}
|
||||
state.chatByProject = map;
|
||||
state.chatId = chatId || "";
|
||||
try {
|
||||
localStorage.setItem(CHAT_BY_PROJECT_KEY, JSON.stringify(state.chatByProject));
|
||||
} catch {
|
||||
// no-op if localStorage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function removePersistedProject(projectId) {
|
||||
persistProjectId("");
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
const map = state.chatByProject && typeof state.chatByProject === "object" ? { ...state.chatByProject } : {};
|
||||
delete map[projectId];
|
||||
state.chatByProject = map;
|
||||
try {
|
||||
localStorage.setItem(CHAT_BY_PROJECT_KEY, JSON.stringify(state.chatByProject));
|
||||
} catch {
|
||||
// no-op if localStorage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function getApiBase() {
|
||||
return (els.apiBase.value || window.location.origin).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
els.healthStatus.textContent = message;
|
||||
}
|
||||
|
||||
function safeStringify(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
function formatMetaTime(requestPayload, created_at, payload) {
|
||||
const rawTime = requestPayload?.createdAt
|
||||
? requestPayload.createdAt
|
||||
: (created_at || payload?.created_at || payload?.timestamp || null);
|
||||
if (!rawTime) {
|
||||
return new Date().toLocaleTimeString();
|
||||
}
|
||||
const dt = new Date(rawTime);
|
||||
return Number.isNaN(dt.getTime()) ? String(rawTime) : dt.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function buildExchangeLog(role, payload) {
|
||||
const transport = payload && typeof payload === "object" ? payload.transport : null;
|
||||
const sections = [];
|
||||
if (role === "user" && transport?.outbound) {
|
||||
sections.push({
|
||||
title: "Что отправили модели",
|
||||
value: transport.outbound,
|
||||
});
|
||||
}
|
||||
if (role === "assistant") {
|
||||
if (transport?.inbound) {
|
||||
sections.push({
|
||||
title: "Что получили от модели",
|
||||
value: transport.inbound,
|
||||
});
|
||||
}
|
||||
if (payload?.raw !== undefined) {
|
||||
sections.push({
|
||||
title: "Полный ответ провайдера",
|
||||
value: payload.raw,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (payload?.route !== undefined) {
|
||||
sections.push({
|
||||
title: "Маршрут модели",
|
||||
value: payload.route,
|
||||
});
|
||||
}
|
||||
if (payload?.provider !== undefined) {
|
||||
sections.push({
|
||||
title: "Провайдер",
|
||||
value: payload.provider,
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderMessage({ role, content, payload, requestPayload, created_at }) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = `message ${role}`;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
const timestamp = formatMetaTime(requestPayload, created_at, payload);
|
||||
meta.textContent = `${role} • ${timestamp}`;
|
||||
const text = document.createElement("div");
|
||||
text.textContent = content || "";
|
||||
wrapper.appendChild(meta);
|
||||
wrapper.appendChild(text);
|
||||
|
||||
const logs = buildExchangeLog(role, payload || {});
|
||||
if (logs.length) {
|
||||
const journal = document.createElement("details");
|
||||
journal.className = "journal";
|
||||
journal.open = false;
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Журнал обмена";
|
||||
journal.appendChild(summary);
|
||||
logs.forEach((item) => {
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "journal-title";
|
||||
heading.textContent = item.title;
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "mono";
|
||||
pre.textContent = safeStringify(item.value);
|
||||
journal.appendChild(heading);
|
||||
journal.appendChild(pre);
|
||||
});
|
||||
wrapper.appendChild(journal);
|
||||
}
|
||||
els.messages.appendChild(wrapper);
|
||||
}
|
||||
|
||||
async function apiRequest(path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const { timeoutMs, ...fetchOptions } = options;
|
||||
const effectiveTimeoutMs = timeoutMs ?? REQUEST_TIMEOUT_MS;
|
||||
const timeout = setTimeout(() => controller.abort(), effectiveTimeoutMs);
|
||||
const url = `${getApiBase()}${path}`;
|
||||
let response;
|
||||
try {
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(fetchOptions.headers || {}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
throw new Error(`Превышен таймаут запроса (${Math.round(effectiveTimeoutMs / 1000)} сек)`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
const bodyText = await response.text();
|
||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||
if (!response.ok) {
|
||||
const message = body?.error?.message || `HTTP ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
const data = await apiRequest("/v1/health");
|
||||
setStatus(`health ok: ${data.status}, service=${data.service}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
const data = await apiRequest("/v1/projects");
|
||||
const projects = data.projects || [];
|
||||
els.projectSelect.innerHTML = "";
|
||||
projects.forEach((project) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = project.id;
|
||||
option.textContent = project.name;
|
||||
els.projectSelect.appendChild(option);
|
||||
});
|
||||
if (!projects.length) {
|
||||
state.projectId = "";
|
||||
state.chatId = "";
|
||||
persistProjectId("");
|
||||
persistChatId("", "");
|
||||
els.projectNameInput.value = "";
|
||||
els.chatNameInput.value = "";
|
||||
els.chatSelect.innerHTML = "";
|
||||
return false;
|
||||
}
|
||||
if (!state.projectId || !projects.some((p) => p.id === state.projectId)) {
|
||||
state.projectId = projects[0].id;
|
||||
}
|
||||
persistProjectId(state.projectId);
|
||||
els.projectSelect.value = state.projectId;
|
||||
const currentProject = projects.find((project) => project.id === state.projectId);
|
||||
if (currentProject) {
|
||||
els.projectNameInput.value = currentProject.name || "";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadChats() {
|
||||
if (!state.projectId) {
|
||||
els.chatSelect.innerHTML = "";
|
||||
state.chatId = "";
|
||||
els.chatNameInput.value = "";
|
||||
return;
|
||||
}
|
||||
const chats = await apiRequest(`/v1/projects/${state.projectId}/chats`);
|
||||
const list = chats.chats || [];
|
||||
els.chatSelect.innerHTML = "";
|
||||
list.forEach((chat) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = chat.id;
|
||||
option.textContent = chat.title;
|
||||
els.chatSelect.appendChild(option);
|
||||
});
|
||||
if (!list.length) {
|
||||
state.chatId = "";
|
||||
els.chatNameInput.value = "";
|
||||
els.chatSelect.innerHTML = "";
|
||||
persistChatId(state.projectId, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const savedChatId = state.chatByProject[state.projectId];
|
||||
if (!state.chatId || !list.some((c) => c.id === state.chatId)) {
|
||||
state.chatId = savedChatId || "";
|
||||
}
|
||||
if (!state.chatId || !list.some((c) => c.id === state.chatId)) {
|
||||
state.chatId = list[0].id;
|
||||
}
|
||||
els.chatSelect.value = state.chatId;
|
||||
const activeChat = list.find((chat) => chat.id === state.chatId);
|
||||
if (activeChat) {
|
||||
els.chatNameInput.value = activeChat.title || "";
|
||||
}
|
||||
persistChatId(state.projectId, state.chatId);
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
if (!state.projectId || !state.chatId) {
|
||||
els.messages.innerHTML = "";
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "message system";
|
||||
if (!state.projectId) {
|
||||
placeholder.textContent = "Нет проекта. Создайте проект вручную.";
|
||||
} else if (!state.chatId) {
|
||||
placeholder.textContent = "В выбранном проекте нет чатов. Создайте чат вручную.";
|
||||
}
|
||||
els.messages.appendChild(placeholder);
|
||||
return;
|
||||
}
|
||||
const data = await apiRequest(`/v1/projects/${state.projectId}/chats/${state.chatId}/messages?limit=200`);
|
||||
const messages = data.messages || [];
|
||||
els.messages.innerHTML = "";
|
||||
const orderedMessages = [...messages].reverse();
|
||||
orderedMessages.forEach((message) => {
|
||||
renderMessage(message);
|
||||
});
|
||||
if (!messages.length) {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "message system";
|
||||
placeholder.textContent = "История чата пуста. Напишите первый вопрос.";
|
||||
els.messages.appendChild(placeholder);
|
||||
}
|
||||
els.messages.scrollTop = 0;
|
||||
}
|
||||
|
||||
async function refreshConversation() {
|
||||
const hasProjects = await loadProjects();
|
||||
if (!hasProjects) {
|
||||
await loadMessages();
|
||||
return;
|
||||
}
|
||||
await loadChats();
|
||||
await loadMessages();
|
||||
}
|
||||
|
||||
async function createChat() {
|
||||
if (!state.projectId) {
|
||||
return;
|
||||
}
|
||||
const created = await apiRequest(`/v1/projects/${state.projectId}/chats`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: els.chatNameInput.value.trim() || `Веб-чат ${new Date().toLocaleTimeString("ru-RU")}`,
|
||||
}),
|
||||
});
|
||||
state.chatId = created.chat.id;
|
||||
persistChatId(state.projectId, state.chatId);
|
||||
await loadChats();
|
||||
await loadMessages();
|
||||
}
|
||||
|
||||
async function sendTurn(messageText) {
|
||||
await apiRequest(`/v1/projects/${state.projectId}/chats/${state.chatId}/turn`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
message: messageText,
|
||||
}),
|
||||
timeoutMs: TURN_TIMEOUT_MS,
|
||||
});
|
||||
await loadMessages();
|
||||
}
|
||||
|
||||
function formatTimeoutMessage(durationMs) {
|
||||
const seconds = Math.max(1, Math.round(durationMs / 1000));
|
||||
return `Ожидаем ответ модели ~${seconds} сек...`;
|
||||
}
|
||||
|
||||
function setSendingFeedback(startTs) {
|
||||
const tick = () => {
|
||||
const elapsed = Date.now() - startTs;
|
||||
if (els.sendButton.disabled) {
|
||||
setStatus(formatTimeoutMessage(elapsed));
|
||||
}
|
||||
};
|
||||
const timer = setInterval(tick, 12000);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
|
||||
els.checkHealthBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await checkHealth();
|
||||
} catch {
|
||||
// handled in function
|
||||
}
|
||||
});
|
||||
|
||||
els.refreshProjectsBtn.addEventListener("click", async () => {
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.createProjectBtn.addEventListener("click", async () => {
|
||||
const now = new Date().toLocaleString("ru-RU");
|
||||
const customName = els.projectNameInput.value.trim();
|
||||
const created = await apiRequest("/v1/projects", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: customName || `Проект ${now}`,
|
||||
description: "Создано из веб-интерфейса",
|
||||
}),
|
||||
});
|
||||
state.projectId = created.project.id;
|
||||
persistProjectId(state.projectId);
|
||||
persistChatId(state.projectId, "");
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.createChatBtn.addEventListener("click", async () => {
|
||||
if (!state.projectId) {
|
||||
return;
|
||||
}
|
||||
await createChat();
|
||||
});
|
||||
|
||||
els.deleteProjectBtn.addEventListener("click", async () => {
|
||||
if (!state.projectId) {
|
||||
return;
|
||||
}
|
||||
const projectName = els.projectNameInput.value.trim() || "текущий проект";
|
||||
if (!window.confirm(`Удалить проект «${projectName}» и все его чаты?`)) {
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/v1/projects/${state.projectId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
removePersistedProject(state.projectId);
|
||||
state.projectId = "";
|
||||
state.chatId = "";
|
||||
els.projectNameInput.value = "";
|
||||
els.chatNameInput.value = "";
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.renameProjectBtn.addEventListener("click", async () => {
|
||||
if (!state.projectId) {
|
||||
return;
|
||||
}
|
||||
const newName = els.projectNameInput.value.trim();
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/v1/projects/${state.projectId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: newName }),
|
||||
});
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.renameChatBtn.addEventListener("click", async () => {
|
||||
if (!state.projectId || !state.chatId) {
|
||||
return;
|
||||
}
|
||||
const newTitle = els.chatNameInput.value.trim();
|
||||
if (!newTitle) {
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/v1/projects/${state.projectId}/chats/${state.chatId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ title: newTitle }),
|
||||
});
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.projectSelect.addEventListener("change", async () => {
|
||||
state.projectId = els.projectSelect.value;
|
||||
state.chatId = "";
|
||||
persistProjectId(state.projectId);
|
||||
persistChatId(state.projectId, "");
|
||||
await refreshConversation();
|
||||
});
|
||||
|
||||
els.chatSelect.addEventListener("change", async () => {
|
||||
state.chatId = els.chatSelect.value;
|
||||
persistChatId(state.projectId, state.chatId);
|
||||
await loadMessages();
|
||||
});
|
||||
|
||||
els.clearMessagesBtn.addEventListener("click", () => {
|
||||
els.messages.innerHTML = "";
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "message system";
|
||||
if (!state.projectId) {
|
||||
placeholder.textContent = "Нет проекта. Создайте проект вручную.";
|
||||
} else if (!state.chatId) {
|
||||
placeholder.textContent = "В выбранном проекте нет чатов. Создайте чат вручную.";
|
||||
} else {
|
||||
placeholder.textContent = "История чата очищена в интерфейсе.";
|
||||
}
|
||||
els.messages.appendChild(placeholder);
|
||||
});
|
||||
|
||||
els.clearChatMemoryBtn.addEventListener("click", async () => {
|
||||
if (!state.projectId || !state.chatId) {
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/v1/projects/${state.projectId}/chats/${state.chatId}/messages`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
await loadMessages();
|
||||
});
|
||||
|
||||
els.messageInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (!els.messageInput.value.trim()) {
|
||||
return;
|
||||
}
|
||||
els.chatForm.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
els.chatForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const messageText = els.messageInput.value.trim();
|
||||
if (!messageText) {
|
||||
return;
|
||||
}
|
||||
if (!state.projectId) {
|
||||
setStatus("Сначала выберите или создайте проект.");
|
||||
return;
|
||||
}
|
||||
if (!state.chatId) {
|
||||
setStatus("Сначала выберите или создайте чат.");
|
||||
return;
|
||||
}
|
||||
|
||||
els.sendButton.disabled = true;
|
||||
setStatus("Отправляю сообщение...");
|
||||
const stopSendingFeedback = setSendingFeedback(Date.now());
|
||||
try {
|
||||
await sendTurn(messageText);
|
||||
els.messageInput.value = "";
|
||||
} catch (error) {
|
||||
setStatus(`Ошибка отправки: ${error.message}`);
|
||||
} finally {
|
||||
stopSendingFeedback();
|
||||
els.sendButton.disabled = false;
|
||||
if (!els.healthStatus.textContent.startsWith("Ошибка отправки") && !els.healthStatus.textContent.startsWith("Ожидаем")) {
|
||||
setStatus("Готов");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
els.apiBase.value = window.location.origin;
|
||||
|
||||
(async function init() {
|
||||
loadStateFromStorage();
|
||||
try {
|
||||
await checkHealth();
|
||||
await refreshConversation();
|
||||
} catch (error) {
|
||||
setStatus(`Не удалось инициализироваться: ${error.message}`);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,220 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f2f3f7;
|
||||
color: #111827;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1400px;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
min-height: calc(100vh - 32px);
|
||||
}
|
||||
|
||||
.section + .section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select,
|
||||
textarea {
|
||||
min-width: 220px;
|
||||
border: 1px solid #9ca3af;
|
||||
border-radius: 6px;
|
||||
padding: 7px 8px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid #9ca3af;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
color: #111827;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
background: #b91c1c;
|
||||
color: #ffffff;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
min-height: 320px;
|
||||
max-height: 64vh;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
border-left: 3px solid #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
border-left: 3px solid #059669;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.message.system {
|
||||
border-left: 3px solid #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.message .meta {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
white-space: pre-wrap;
|
||||
overflow: auto;
|
||||
max-height: 220px;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.inline-toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.journal {
|
||||
margin-top: 8px;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.journal > summary {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.journal-title {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>1С Agent</title>
|
||||
<link rel="stylesheet" href="/ui/assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="sidebar panel" aria-label="Боковая панель настроек">
|
||||
<h1>1С Agent</h1>
|
||||
<p>Раздел настроек</p>
|
||||
|
||||
<section class="section">
|
||||
<h2>Соединение</h2>
|
||||
<div class="row">
|
||||
<label>
|
||||
API URL
|
||||
<input id="apiBase" type="text" value="" />
|
||||
</label>
|
||||
</div>
|
||||
<button id="checkHealth" class="button button-primary">Проверить /v1/health</button>
|
||||
<div class="status" id="healthStatus">Инициализация…</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Контекст</h2>
|
||||
<label>
|
||||
Новое имя проекта
|
||||
<input id="projectName" type="text" value="" placeholder="Название проекта" />
|
||||
</label>
|
||||
<label>
|
||||
Проект
|
||||
<select id="projectSelect"></select>
|
||||
</label>
|
||||
<div class="row">
|
||||
<button id="renameProject" class="button">Переименовать</button>
|
||||
<button id="refreshProjects" class="button">Обновить</button>
|
||||
<button id="createProject" class="button button-primary">Новый проект</button>
|
||||
<button id="deleteProject" class="button button-danger" type="button">Удалить проект</button>
|
||||
</div>
|
||||
<label>
|
||||
Новое имя чата
|
||||
<input id="chatName" type="text" value="" placeholder="Название чата" />
|
||||
</label>
|
||||
<label>
|
||||
Чат
|
||||
<select id="chatSelect"></select>
|
||||
</label>
|
||||
<div class="row">
|
||||
<button id="renameChat" class="button">Переименовать</button>
|
||||
<button id="createChat" class="button button-primary">Новый чат</button>
|
||||
<button id="clearMessages" class="button" type="button">Очистить экран</button>
|
||||
<button id="clearChatMemory" class="button button-danger" type="button">Очистить чат полностью</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</aside>
|
||||
|
||||
<section class="chat-area">
|
||||
<section class="panel">
|
||||
<h2>Чат</h2>
|
||||
<form id="chatForm" class="composer">
|
||||
<textarea id="messageInput" rows="2" placeholder="Введите вопрос по 1С"></textarea>
|
||||
<div class="composer-actions">
|
||||
<button id="sendButton" class="button button-primary" type="submit">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel chat-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/ui/assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user