Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/+cli-rest.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
提供独立 CLI 使用的普通 REST 管理与检索接口、动态输入 schema 发现、原生二进制与 multipart 内容交付;Job 新增 best-effort 停止请求,显式 rumination 通过 Job 受理。减少已验证配置在内部传递时的重复校验。
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
docs/
tasks/
tests/
cli/

data/extensions/
extensions/*/pdm.lock
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ env:
PDM_CHECK_UPDATE: "false"

jobs:
cli:
name: inkcre-cli checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
with:
python-version: '3.12'
version: 2.28.0
- name: 检查独立依赖、静态合同和分发构建
run: bash scripts/automation/cli_publication.sh check-build

repository:
name: Hermetic repository contract
runs-on: ubuntu-latest
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/cli-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Publish inkcre-cli

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: publish-inkcre-cli
cancel-in-progress: false

jobs:
select:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
selected: ${{ steps.selection.outputs.selected }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ github.sha }}
fetch-depth: 0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: 选择已准备的新 CLI 版本
id: selection
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
run: bash scripts/automation/cli_publication.sh select

publish:
needs: select
if: needs.select.outputs.selected == 'true'
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ github.sha }}
- uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
with:
python-version: '3.12'
version: 2.28.0
- name: 独立检查并构建本次发布产物
run: bash scripts/automation/cli_publication.sh check-build
- name: 使用 PyPI Trusted Publishing 上传
run: bash scripts/automation/cli_publication.sh publish
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ repos:
entry: ruff format --check .
language: system
files: ^(.*\.py|pyproject\.toml|ruff\.toml)$
exclude: ^(cli/|\.agents/)
pass_filenames: false
- id: typecheck
name: Check Pyrefly types
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ authorize new unit, schema, helper, mocked-manager, or route tests by analogy.

## Release intent

Core and each first-party Extension are independent release projects. A feature pull request that
Core, `cli/`, and each first-party Extension are independent release projects. A feature pull request that
changes delivered project behavior adds at least one non-empty project-local Towncrier fragment;
it does not change a version or generated changelog:

Expand All @@ -49,7 +49,7 @@ pdm run towncrier create --config towncrier.toml --dir extensions/<extension-id>
pdm run check:releases --base origin/main
```

Core fragments live in `.changes/`; Extension fragments live in
Core fragments live in `.changes/`; CLI fragments live in `cli/.changes/`; Extension fragments live in
`extensions/<extension-id>/.changes/`. Valid types are `added`, `changed`, `deprecated`, `removed`,
`fixed`, and `security`. Pure release-tooling or contributor-documentation changes do not invent
project news.
Expand Down
88 changes: 87 additions & 1 deletion app/business/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
import typing

import pydantic
import sqlmodel

from app.business.ai import AIExecutionRequirement, AIManager
from app.engine import SessionLocal
from app.schemas import AgentDefinitionModel
from app.schemas.agent import AgentID
from app.schemas.agent import AgentID, AgentForm, AgentUpdateForm
from app.schemas.ai import FunctionTool, SystemMessage, UserMessage

from .contracts import (
Expand Down Expand Up @@ -70,6 +71,91 @@ class AgentManager:
_TOOLS: dict[str, _ToolRegistration] = {}
_persistence: ThreadPersistenceBackend = InMemoryThreadPersistenceBackend()

@classmethod
def get_definition(cls, agent_id: AgentID) -> AgentDefinitionModel | None:
with SessionLocal() as db:
return db.get(AgentDefinitionModel, agent_id)

@classmethod
def list_definitions(
cls, *, limit: int | None = None, cursor: int | None = None
) -> tuple[list[AgentDefinitionModel], int | None]:
statement = sqlmodel.select(AgentDefinitionModel).order_by(
sqlmodel.col(AgentDefinitionModel.id)
)
if cursor is not None:
statement = statement.where(sqlmodel.col(AgentDefinitionModel.id) > cursor)
if limit is not None:
statement = statement.limit(limit + 1)
with SessionLocal() as db:
rows = list(db.exec(statement).all())
more = limit is not None and len(rows) > limit
rows = rows[:limit]
return rows, rows[-1].id if more else None

@classmethod
def create_definition(cls, form: AgentForm) -> AgentDefinitionModel:
with SessionLocal() as db:
record = AgentDefinitionModel(**form.model_dump())
db.add(record)
db.commit()
db.refresh(record)
return record

@classmethod
def update_definition(
cls, agent_id: AgentID, form: AgentUpdateForm
) -> AgentDefinitionModel:
with SessionLocal() as db:
record = db.exec(
sqlmodel.select(AgentDefinitionModel)
.where(AgentDefinitionModel.id == agent_id)
.with_for_update()
).one_or_none()
if record is None:
raise AgentNotFoundError(f"Agent {agent_id} does not exist")
changes = form.model_dump(exclude_unset=True)
candidate = AgentForm.model_validate(
{
**{field: getattr(record, field) for field in AgentForm.model_fields},
**changes,
}
)
for field in changes:
setattr(record, field, getattr(candidate, field))
db.add(record)
db.commit()
db.refresh(record)
return record

@classmethod
def delete_definition(cls, agent_id: AgentID) -> bool:
with SessionLocal() as db:
record = db.get(AgentDefinitionModel, agent_id)
if record is None:
return False
db.delete(record)
db.commit()
return True

@classmethod
def list_tools(
cls, *, limit: int | None = None, cursor: str | None = None
) -> tuple[list[dict[str, str]], str | None]:
ids = sorted(key for key in cls._TOOLS if cursor is None or key > cursor)
more = limit is not None and len(ids) > limit
ids = ids[:limit]
return [{"id": key, "description": cls._TOOLS[key].description} for key in ids], ids[
-1
] if more else None

@classmethod
def get_tool(cls, tool_id: str) -> FunctionTool:
registration = cls._TOOLS.get(tool_id)
if registration is None:
raise MissingAgentToolError(f"Agent Tool {tool_id!r} is not registered")
return registration.bind(tool_id).definition

@classmethod
def can_execute(cls, agent_id: AgentID, input_modality: str) -> bool:
"""Return static local eligibility for one Agent and canonical input modality."""
Expand Down
2 changes: 1 addition & 1 deletion app/business/ai/dialects/openai_compatible.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _create_client(config: OpenAICompatibleConfig) -> AsyncOpenAI:

@staticmethod
def _config(config: pydantic.BaseModel) -> OpenAICompatibleConfig:
return OpenAICompatibleConfig.model_validate(config)
return typing.cast(OpenAICompatibleConfig, config)

async def embed(
self,
Expand Down
22 changes: 22 additions & 0 deletions app/business/ai/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pydantic
import sqlalchemy.dialects.postgresql
import sqlmodel

from app.database_contract.profile import BUILTIN_AI_DIALECTS_BY_ID
from app.engine import SessionLocal
Expand Down Expand Up @@ -73,6 +74,27 @@ class AIManager:

_DIALECTS: dict[AIDialectID, _DialectRegistration] = {}

@classmethod
def get_model(cls, model_id: AIModelID) -> AIModelModel | None:
"""Read a model record without requiring its provider/dialect to execute here."""
with SessionLocal() as db:
return db.get(AIModelModel, model_id)

@classmethod
def list_models(
cls, *, limit: int | None = None, cursor: int | None = None
) -> tuple[list[AIModelModel], int | None]:
statement = sqlmodel.select(AIModelModel).order_by(sqlmodel.col(AIModelModel.id))
if cursor is not None:
statement = statement.where(sqlmodel.col(AIModelModel.id) > cursor)
if limit is not None:
statement = statement.limit(limit + 1)
with SessionLocal() as db:
rows = list(db.exec(statement).all())
more = limit is not None and len(rows) > limit
rows = rows[:limit]
return rows, rows[-1].id if more else None

@classmethod
def register_dialect(
cls,
Expand Down
Loading
Loading