Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .changes/core-app-separation.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
新增可独立构建的 inkcre_core SDK,显式绑定数据库与 Peer 执行上下文;服务保留环境、HTTP、调度和安装装配,旧扩展导入继续兼容。
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
cache: true

- name: Install locked dependencies
run: pdm install -G dev --frozen-lockfile
run: pdm install -G app -G dev --frozen-lockfile

- name: Enforce release intent
env:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/extension-documentation-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
cache: true

- name: Install frozen publisher
run: pdm install --no-default --group extension-publisher --frozen-lockfile
run: pdm install --no-self --no-default --group extension-publisher --frozen-lockfile

- name: Set up pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
cache: true

- name: Install frozen publisher
run: pdm install --no-default --group extension-publisher --frozen-lockfile
run: pdm install --no-self --no-default --group extension-publisher --frozen-lockfile

- name: Restore exact documentation candidates
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/extension-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
- name: Install frozen publisher
if: steps.selection.outputs.selected == 'true'
run: >-
pdm install --no-default --group extension-publisher --frozen-lockfile
pdm install --no-self --no-default --group extension-publisher --frozen-lockfile

- name: Build and verify native wheel
if: steps.selection.outputs.selected == 'true'
Expand Down Expand Up @@ -203,7 +203,7 @@ jobs:
- name: Install frozen publisher
if: steps.candidate.outputs.selected == 'true'
run: >-
pdm install --no-default --group extension-publisher --frozen-lockfile
pdm install --no-self --no-default --group extension-publisher --frozen-lockfile

- name: Prepare exact native Release association
if: steps.candidate.outputs.selected == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
- name: Install frozen Extension preview tooling
working-directory: source
run: >-
pdm install --no-default --group extension-preview --frozen-lockfile
pdm install --no-self --no-default --group extension-preview --frozen-lockfile

- name: Build exact-head Extension wheels and sibling Registry facade
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
cache: true

- name: Install locked release tooling
run: pdm install -G dev --frozen-lockfile
run: pdm install -G app -G dev --frozen-lockfile

- name: Prepare every pending project
run: pdm run release:prepare
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/self-host-render-neon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
cache: true

- name: Install locked runtime dependencies
run: pdm install --prod --no-editable --frozen-lockfile
run: pdm install -G app --prod --no-editable --frozen-lockfile

- name: Resolve the Neon default branch owner coordinates
id: neon
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ Before choosing a design, identify shared state, business contracts, and each pa
## Repository Map

- `run.py`: FastAPI bootstrap
- `app/`: application routes, schemas, and business units
- `inkcre_core/`: independently consumable SDK, models, domain operations, persistence and execution
- `app/`: service routes, configuration, extension/sink composition and legacy import shims
- `cli/`: `inkcre-cli` implementation
- `extensions/`: built-in first-party extension packages
- `libs/`: shared libraries
- `libs/`, `utils/`: legacy imports; implementations live under `inkcre_core/`
- `migrations/`: Alembic revisions and integrity baseline (append-only after deployment)
- `tests/`: migration integrity and mature integration/acceptance checks
- `docs/30-unit-tdd/`: `impl-py` internal design
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ bugs that do not cross a security boundary.

```bash
git submodule update --init --recursive
pdm install -G dev --frozen-lockfile
pdm install -G app -G dev --frozen-lockfile
pdm run pre-commit install
pdm run doctor
pdm run check
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ WORKDIR /app
# PDM 2.28 requires the httpx client replaced in unearth 0.19.
RUN pip install --no-cache-dir "pdm==${PDM_VERSION}" "unearth==0.18.3"

COPY pyproject.toml pdm.lock README.md ./
RUN pdm install --prod --no-editable --frozen-lockfile
COPY pyproject.toml pdm.lock README.md LICENSE ./
COPY inkcre_core/ inkcre_core/
RUN pdm install -G app --prod --no-editable --frozen-lockfile


FROM python:${PYTHON_VERSION}-slim AS runtime
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ You do not need to clone this repository. Fork it on GitHub, configure the docum

```bash
git submodule update --init --recursive
pdm install -G dev --frozen-lockfile
pdm install -G app -G dev --frozen-lockfile
svc dev ensure database --repo . --json
pdm run dev
```
Expand All @@ -40,3 +40,28 @@ Report vulnerabilities privately through [SECURITY.md](SECURITY.md). Security de

- OpenAPI schema: `docs/openapi.json` (regenerate with `pdm run python scripts/generate-openapi.py`)
- This repository does not publish hosted API documentation automatically

## 独立 Python SDK

构建 `pdm build` 得到 `inkcre-core` wheel,安装后使用 `inkcre_core`。基础依赖不包含 FastAPI、Uvicorn、APScheduler 或扩展安装器;服务开发使用 `pdm install -G app -G dev`。Core 当前支持 Python 3.12、PostgreSQL/psycopg,数据库 schema 和迁移仍由本仓 App 运维入口管理。

```python
from inkcre_core import Core
from inkcre_core.business.info_base import BlockService

async def read_block(database_url: str, block_id: int):
core = Core(database_url)
try:
with core.bind():
return await BlockService.get(block_id)
finally:
await core.aclose()
```

`Core` 构造不连接数据库或启动执行者。调用方传入数据库 URL 或借用的 `AsyncEngine`,用 `CoreOptions` 提供 Peer 身份、认证和调试参数。绑定按异步任务隔离,子任务继承;每次领域操作仍建立自己的短事务。A、B 实例可以并行操作不同信息库,应用切换配置不改变既有实例。关闭会收束该实例的 Job,释放自建 engine;借用 engine 的关闭权仍属于调用方。调用方应先结束自己的操作再关闭实例。

公开消费面是 `Core` / `CoreOptions`、`inkcre_core.schemas` 的模型,以及各 `inkcre_core.business` 包导出的领域入口(例如 `BlockService`、`submit_graph`)。`persistence` 和私有实现模块属于内部细节。普通读写不需要启动 Job、恢复扩展或加载 `app.settings`;使用 Resolver 时可显式注册所需实现。Job 的启动和轮询由调用方决定。

HTTP 路由、环境读取、调度频率、扩展安装/进程重启和 Sink 的服务挂载由 `app/` 保留。已有独立 Extension runtime 的生命周期合同保持原 owner。原 `app.business.*`、`app.schemas.*`、`libs.*`、`utils.*` 导入作为服务的兼容入口保留,历史迁移与已发布扩展仍指向同一实现;独立 wheel 只交付 `inkcre_core`,不交付这些旧服务路径。

本地边界检查:`pdm run check:core-package` 构建 wheel 并在仓库之外检查独立消费,`pdm run python -m pytest -q tests/test_core_context.py` 验证并行绑定与资源所有权;两者均不连接数据库,不属于端到端验收。
4 changes: 2 additions & 2 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Application Core
# Service Application

> Applies to `app/`; deeper guides add narrower constraints.

- Authority and dependency direction are documented in `docs/30-unit-tdd/business-pipeline-and-authority.md`; do not reconstruct them from directory names.
- Settings enter through `app/settings.py`; database sessions enter through `app/engine.py`. Do not create a second process-global settings, engine, or session authority.
- Settings enter through `app/settings.py`; `app/core.py` composes and binds the service-owned SDK instance. `app/engine.py` only preserves old service imports. Domain operations use the bound Core and own short UoWs.
- Runtime registration and database effects belong in lifespan/bootstrap paths, not module import. Preserve import-only OpenAPI generation.
- Routes parse transport input and call owning business managers; do not duplicate business policy in routes.
- Required check: run the narrow tests for the changed unit, then `pdm run check:foundation` for bootstrap, dependency, or import-boundary changes.
13 changes: 2 additions & 11 deletions app/business/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
# Business Units
# Service-owned execution

> Applies to `app/business/`; deeper guides own local hazards.

- Read `docs/30-unit-tdd/business-pipeline-and-authority.md` before a change crosses business subtrees.
- `info_base` owns graph persistence; sources and extensions propose commands but do not copy generic persistence.
- AI execution remains graph-blind. Agent and organization layers may compose it but must not move graph or resolver policy into AI.
- Semantic and lexical retrieval own separate derived records and ranking policy; neither owns graph facts or mutates the graph while reading.
- Global Job/Cron machinery owns durable execution occurrence and claim lifecycle; domain units retain typed payload and effect semantics.
- Peer owns identity, capability, lease, and delegation transport facts; typed business codecs remain with their business owner.
- Application use cases choose UoW scopes; raw sessions stay in persistence. Composed operations require a UoW and never end its transaction. Each concurrent task owns a separate UoW; external I/O runs outside DB scopes.
- Required check: run tests for every affected business owner and the repository import-boundary checks.
Extension installation/restart and FastAPI Sink composition belong here. Reusable domain operations and transaction ownership live in `inkcre_core`; remaining old domain modules only preserve published imports. Do not add application settings, scheduler or HTTP dependencies to the SDK.
41 changes: 2 additions & 39 deletions app/business/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,3 @@
"""Reusable Agent definitions, typed Tools and in-memory Thread execution."""
"""Legacy import; use inkcre_core for new consumers."""

from .contracts import (
AgentError,
AgentNotFoundError,
AgentToolBindingError,
AgentTurnActiveError,
BoundAgentTool,
DuplicateAgentToolRegistrationError,
MissingAgentToolError,
ToolExecutionError,
)
from .main import AgentManager
from .bootstrap import register_core_agent_tools
from .persistence import (
InMemoryThreadPersistenceBackend,
ThreadID,
ThreadPersistenceBackend,
ThreadState,
)
from .thread import Thread, TurnTermination

__all__ = [
"AgentError",
"AgentManager",
"register_core_agent_tools",
"AgentNotFoundError",
"AgentToolBindingError",
"AgentTurnActiveError",
"BoundAgentTool",
"DuplicateAgentToolRegistrationError",
"InMemoryThreadPersistenceBackend",
"MissingAgentToolError",
"Thread",
"ThreadID",
"ThreadPersistenceBackend",
"ThreadState",
"ToolExecutionError",
"TurnTermination",
]
from inkcre_core.business.agent import *
21 changes: 7 additions & 14 deletions app/business/agent/bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
"""Explicit registration entrypoint for core-owned Agent Tool controllers."""
"""Legacy import; use inkcre_core for new consumers."""

from typing import TYPE_CHECKING
from importlib import import_module
import sys

def register_core_agent_tools() -> None:
"""Import every core Tool owner, including Sink delivery controllers.
sys.modules[__name__] = import_module("inkcre_core.business.agent.bootstrap")

Decorators register Tools when their modules load; Python's import cache makes
repeated bootstrap calls harmless. Entry points must call this explicitly,
rather than rely on routes or package re-exports importing the owners first.
"""
from app.business.graph_navigation_retrieval import tools as _graph_tools
from app.business.info_base import tools as _info_base_tools
from app.business.info_base.resolver import tools as _resolver_tools
from app.business.organization import tools as _organization_tools
from app.business.sink import agent_query as _agent_query

del _graph_tools, _info_base_tools, _resolver_tools, _organization_tools, _agent_query
if TYPE_CHECKING:
from inkcre_core.business.agent.bootstrap import *
63 changes: 7 additions & 56 deletions app/business/agent/contracts.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,10 @@
"""Agent Tool binding and runtime failure contracts."""
"""Legacy import; use inkcre_core for new consumers."""

from __future__ import annotations
from typing import TYPE_CHECKING
from importlib import import_module
import sys

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import typing
sys.modules[__name__] = import_module("inkcre_core.business.agent.contracts")

import pydantic

from app.schemas.ai import FunctionTool, JSONValue


AgentToolHandler: typing.TypeAlias = Callable[
[pydantic.BaseModel],
JSONValue | Awaitable[JSONValue],
]
AgentToolInputModelFactory: typing.TypeAlias = Callable[[], type[pydantic.BaseModel]]


class AgentError(RuntimeError):
"""Base failure exposed by the Agent domain."""


class AgentNotFoundError(AgentError):
pass


class AgentToolBindingError(AgentError):
pass


class DuplicateAgentToolRegistrationError(AgentToolBindingError):
pass


class MissingAgentToolError(AgentToolBindingError):
pass


class AgentTurnActiveError(AgentError):
pass


class ToolExecutionError(Exception):
"""A Tool-owned actionable failure intended for the model."""

def __init__(self, content: JSONValue):
super().__init__("Agent Tool execution failed")
self.content = content


@dataclass(frozen=True)
class BoundAgentTool:
"""One run-local Tool schema, validation model and code-owned handler."""

definition: FunctionTool
input_model: type[pydantic.BaseModel]
handler: AgentToolHandler
if TYPE_CHECKING:
from inkcre_core.business.agent.contracts import *
53 changes: 6 additions & 47 deletions app/business/agent/debug.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,10 @@
"""Opt-in development traces through the existing logging backend."""
"""Legacy import; use inkcre_core for new consumers."""

import asyncio
import datetime
import json
from typing import TYPE_CHECKING
from importlib import import_module
import sys
import typing
import uuid

import pydantic
sys.modules[__name__] = import_module("inkcre_core.business.agent.debug")

from app.settings import settings
from libs.obsrv.log_record import ENABLE_LOG_BACKEND, TRACE_ID
from libs.obsrv.main import get_logger


def _json_value(value: typing.Any) -> typing.Any:
if isinstance(value, pydantic.BaseModel):
return value.model_dump(mode="python")
if isinstance(value, bytes):
return {"omitted_binary_bytes": len(value)}
if isinstance(value, datetime.datetime | datetime.date):
return value.isoformat()
if isinstance(value, uuid.UUID):
return str(value)
raise TypeError(f"Unsupported Agent trace value: {type(value).__name__}")


def _emit(event: str, thread_id: str, payload: dict[str, typing.Any]) -> None:
token = ENABLE_LOG_BACKEND.set(True)
try:
envelope = {
"event": event,
"thread_id": thread_id,
"trace_id": TRACE_ID.get(),
**payload,
}
get_logger().getChild("agent.debug").info(
json.dumps(envelope, ensure_ascii=False, default=_json_value),
extra={"event": event, "agent_thread_id": thread_id},
)
except Exception as error:
# A debug destination must never replace the Agent's real outcome.
sys.stderr.write(f"Agent debug trace unavailable: {type(error).__name__}\n")
finally:
ENABLE_LOG_BACKEND.reset(token)


async def trace(event: str, thread_id: uuid.UUID, **payload: typing.Any) -> None:
if settings.obsrv.agent_debug:
await asyncio.to_thread(_emit, event, str(thread_id), payload)
if TYPE_CHECKING:
from inkcre_core.business.agent.debug import *
Loading
Loading