tilo 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tilo-0.1.0/.gitignore +40 -0
- tilo-0.1.0/Dockerfile +12 -0
- tilo-0.1.0/PKG-INFO +24 -0
- tilo-0.1.0/alembic/__init__.py +0 -0
- tilo-0.1.0/alembic/env.py +62 -0
- tilo-0.1.0/alembic/versions/0001_initial_schema.py +31 -0
- tilo-0.1.0/alembic/versions/0002_v02_columns.py +99 -0
- tilo-0.1.0/alembic.ini +43 -0
- tilo-0.1.0/pyproject.toml +45 -0
- tilo-0.1.0/tests/helpers.py +92 -0
- tilo-0.1.0/tests/test_aip_generator.py +64 -0
- tilo-0.1.0/tests/test_apps_and_policy.py +342 -0
- tilo-0.1.0/tests/test_artifact_action_runtime.py +329 -0
- tilo-0.1.0/tests/test_artifact_spec.py +87 -0
- tilo-0.1.0/tests/test_behaviour_memory.py +383 -0
- tilo-0.1.0/tests/test_conversation_runtime.py +282 -0
- tilo-0.1.0/tests/test_demo_contract.py +391 -0
- tilo-0.1.0/tests/test_health_and_runtime.py +122 -0
- tilo-0.1.0/tests/test_langchain_adapter.py +237 -0
- tilo-0.1.0/tests/test_mcp_adapter.py +62 -0
- tilo-0.1.0/tests/test_memory_lifecycle.py +53 -0
- tilo-0.1.0/tests/test_model_runtime.py +83 -0
- tilo-0.1.0/tests/test_policy_runtime_integration.py +395 -0
- tilo-0.1.0/tests/test_run_streaming.py +406 -0
- tilo-0.1.0/tests/test_surface_schema.py +635 -0
- tilo-0.1.0/tilo/__init__.py +1 -0
- tilo-0.1.0/tilo/adapters/__init__.py +5 -0
- tilo-0.1.0/tilo/adapters/a2a.py +29 -0
- tilo-0.1.0/tilo/adapters/acp.py +29 -0
- tilo-0.1.0/tilo/adapters/langchain.py +347 -0
- tilo-0.1.0/tilo/adapters/mcp.py +125 -0
- tilo-0.1.0/tilo/api/__init__.py +1 -0
- tilo-0.1.0/tilo/api/deps.py +20 -0
- tilo-0.1.0/tilo/api/routes/__init__.py +22 -0
- tilo-0.1.0/tilo/api/routes/agents.py +40 -0
- tilo-0.1.0/tilo/api/routes/apps.py +55 -0
- tilo-0.1.0/tilo/api/routes/artifacts.py +83 -0
- tilo-0.1.0/tilo/api/routes/channels.py +25 -0
- tilo-0.1.0/tilo/api/routes/confirmations.py +57 -0
- tilo-0.1.0/tilo/api/routes/conversations.py +142 -0
- tilo-0.1.0/tilo/api/routes/demo.py +19 -0
- tilo-0.1.0/tilo/api/routes/feedback.py +34 -0
- tilo-0.1.0/tilo/api/routes/interactions.py +58 -0
- tilo-0.1.0/tilo/api/routes/memories.py +146 -0
- tilo-0.1.0/tilo/api/routes/messages.py +52 -0
- tilo-0.1.0/tilo/api/routes/projects.py +40 -0
- tilo-0.1.0/tilo/api/routes/runs.py +42 -0
- tilo-0.1.0/tilo/api/routes/skills.py +104 -0
- tilo-0.1.0/tilo/api/routes/system.py +44 -0
- tilo-0.1.0/tilo/api/routes/tasks.py +54 -0
- tilo-0.1.0/tilo/api/routes/tools.py +67 -0
- tilo-0.1.0/tilo/api/routes/workspaces.py +40 -0
- tilo-0.1.0/tilo/cli.py +107 -0
- tilo-0.1.0/tilo/core/__init__.py +1 -0
- tilo-0.1.0/tilo/core/config.py +46 -0
- tilo-0.1.0/tilo/core/database.py +24 -0
- tilo-0.1.0/tilo/core/migrations.py +67 -0
- tilo-0.1.0/tilo/core/time.py +5 -0
- tilo-0.1.0/tilo/main.py +93 -0
- tilo-0.1.0/tilo/models/__init__.py +53 -0
- tilo-0.1.0/tilo/models/domain.py +404 -0
- tilo-0.1.0/tilo/schemas/__init__.py +3 -0
- tilo-0.1.0/tilo/schemas/artifact.py +241 -0
- tilo-0.1.0/tilo/schemas/domain.py +533 -0
- tilo-0.1.0/tilo/schemas/surface.py +504 -0
- tilo-0.1.0/tilo/services/__init__.py +1 -0
- tilo-0.1.0/tilo/services/agent_context/__init__.py +3 -0
- tilo-0.1.0/tilo/services/agent_context/builder.py +127 -0
- tilo-0.1.0/tilo/services/agent_runtime/__init__.py +3 -0
- tilo-0.1.0/tilo/services/agent_runtime/executor.py +38 -0
- tilo-0.1.0/tilo/services/agent_runtime/message_flow.py +116 -0
- tilo-0.1.0/tilo/services/agent_runtime/planner.py +99 -0
- tilo-0.1.0/tilo/services/agent_runtime/prompt_builder.py +72 -0
- tilo-0.1.0/tilo/services/agent_runtime/run_manager.py +457 -0
- tilo-0.1.0/tilo/services/agent_runtime/state_machine.py +87 -0
- tilo-0.1.0/tilo/services/apps/__init__.py +4 -0
- tilo-0.1.0/tilo/services/apps/loader.py +88 -0
- tilo-0.1.0/tilo/services/apps/schemas.py +48 -0
- tilo-0.1.0/tilo/services/artifact/__init__.py +3 -0
- tilo-0.1.0/tilo/services/artifact/actions.py +523 -0
- tilo-0.1.0/tilo/services/artifact/aip_generator.py +887 -0
- tilo-0.1.0/tilo/services/artifact/contract_llm.py +114 -0
- tilo-0.1.0/tilo/services/artifact/generator.py +296 -0
- tilo-0.1.0/tilo/services/artifact/persistence.py +44 -0
- tilo-0.1.0/tilo/services/artifact/spec.py +923 -0
- tilo-0.1.0/tilo/services/bootstrap.py +50 -0
- tilo-0.1.0/tilo/services/channels/__init__.py +2 -0
- tilo-0.1.0/tilo/services/channels/telegram/__init__.py +4 -0
- tilo-0.1.0/tilo/services/channels/telegram/adapter.py +81 -0
- tilo-0.1.0/tilo/services/channels/telegram/renderer.py +59 -0
- tilo-0.1.0/tilo/services/channels/telegram/types.py +18 -0
- tilo-0.1.0/tilo/services/channels/telegram/webhook.py +259 -0
- tilo-0.1.0/tilo/services/channels/types.py +64 -0
- tilo-0.1.0/tilo/services/context_reflection/__init__.py +11 -0
- tilo-0.1.0/tilo/services/context_reflection/schemas.py +31 -0
- tilo-0.1.0/tilo/services/context_reflection/service.py +224 -0
- tilo-0.1.0/tilo/services/conversations/__init__.py +3 -0
- tilo-0.1.0/tilo/services/conversations/constants.py +25 -0
- tilo-0.1.0/tilo/services/conversations/messages.py +121 -0
- tilo-0.1.0/tilo/services/conversations/service.py +190 -0
- tilo-0.1.0/tilo/services/demo/__init__.py +6 -0
- tilo-0.1.0/tilo/services/demo/contracts.py +41 -0
- tilo-0.1.0/tilo/services/improvement/__init__.py +4 -0
- tilo-0.1.0/tilo/services/improvement/candidates.py +144 -0
- tilo-0.1.0/tilo/services/improvement/metrics.py +50 -0
- tilo-0.1.0/tilo/services/inbox/__init__.py +3 -0
- tilo-0.1.0/tilo/services/inbox/confirmations.py +77 -0
- tilo-0.1.0/tilo/services/interaction_policy/__init__.py +3 -0
- tilo-0.1.0/tilo/services/interaction_policy/schemas.py +125 -0
- tilo-0.1.0/tilo/services/interaction_policy/service.py +161 -0
- tilo-0.1.0/tilo/services/interactions/__init__.py +1 -0
- tilo-0.1.0/tilo/services/interactions/events.py +51 -0
- tilo-0.1.0/tilo/services/memory/__init__.py +4 -0
- tilo-0.1.0/tilo/services/memory/behaviour.py +320 -0
- tilo-0.1.0/tilo/services/memory/extraction.py +124 -0
- tilo-0.1.0/tilo/services/memory/recall.py +209 -0
- tilo-0.1.0/tilo/services/memory/writer.py +160 -0
- tilo-0.1.0/tilo/services/models/__init__.py +11 -0
- tilo-0.1.0/tilo/services/models/client.py +416 -0
- tilo-0.1.0/tilo/services/models/errors.py +18 -0
- tilo-0.1.0/tilo/services/models/prompts.py +105 -0
- tilo-0.1.0/tilo/services/models/schemas.py +129 -0
- tilo-0.1.0/tilo/services/skill/__init__.py +3 -0
- tilo-0.1.0/tilo/services/skill/selector.py +27 -0
- tilo-0.1.0/tilo/services/surface/__init__.py +23 -0
- tilo-0.1.0/tilo/services/surface/composer.py +519 -0
- tilo-0.1.0/tilo/services/surface/persistence.py +145 -0
- tilo-0.1.0/tilo/services/surfaces/__init__.py +3 -0
- tilo-0.1.0/tilo/services/surfaces/constants.py +13 -0
- tilo-0.1.0/tilo/services/surfaces/rich_links.py +31 -0
- tilo-0.1.0/tilo/services/tools/__init__.py +3 -0
- tilo-0.1.0/tilo/services/tools/invocation.py +129 -0
- tilo-0.1.0/tilo/services/tools/registry.py +40 -0
- tilo-0.1.0/tilo/services/trace/__init__.py +3 -0
- tilo-0.1.0/tilo/services/trace/recorder.py +160 -0
tilo-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.pyo
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
node_modules/
|
|
14
|
+
.next/
|
|
15
|
+
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
|
|
20
|
+
*.db
|
|
21
|
+
*.db-shm
|
|
22
|
+
*.db-wal
|
|
23
|
+
|
|
24
|
+
evals/reports/*.json
|
|
25
|
+
!evals/reports/.gitkeep
|
|
26
|
+
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
*.swp
|
|
30
|
+
*.swo
|
|
31
|
+
.logs/
|
|
32
|
+
|
|
33
|
+
.codebuddy/
|
|
34
|
+
|
|
35
|
+
# Demo recordings live in GitHub Releases, never the repo.
|
|
36
|
+
# (See docs/demos/README.md for the canonical URLs.)
|
|
37
|
+
*.mp4
|
|
38
|
+
*.mov
|
|
39
|
+
*.webm
|
|
40
|
+
*.m4v
|
tilo-0.1.0/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY backend/pyproject.toml ./
|
|
6
|
+
RUN pip install --no-cache-dir .
|
|
7
|
+
|
|
8
|
+
COPY backend/tilo ./tilo
|
|
9
|
+
COPY examples ./examples
|
|
10
|
+
|
|
11
|
+
EXPOSE 8000
|
|
12
|
+
CMD ["uvicorn", "tilo.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
tilo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tilo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tilo — AI-native SaaS agent framework with long-term memory, structured artifacts, and human confirmation.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agent,ai,artifact,framework,memory,saas
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Framework :: FastAPI
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: alembic>=1.13
|
|
15
|
+
Requires-Dist: fastapi>=0.110
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
18
|
+
Requires-Dist: pydantic-settings>=2.2
|
|
19
|
+
Requires-Dist: pydantic>=2.6
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Alembic environment configuration for Tilo Framework.
|
|
2
|
+
|
|
3
|
+
The database URL is read from tilo.core.config (DATABASE_URL env var).
|
|
4
|
+
All ORM models are imported here so Alembic can detect schema changes
|
|
5
|
+
for `alembic revision --autogenerate`.
|
|
6
|
+
"""
|
|
7
|
+
from logging.config import fileConfig
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import engine_from_config, pool
|
|
10
|
+
from alembic import context
|
|
11
|
+
|
|
12
|
+
# Alembic Config provides access to alembic.ini values.
|
|
13
|
+
config = context.config
|
|
14
|
+
|
|
15
|
+
if config.config_file_name is not None:
|
|
16
|
+
fileConfig(config.config_file_name)
|
|
17
|
+
|
|
18
|
+
# Import Base and register all models so target_metadata is complete.
|
|
19
|
+
from tilo.core.database import Base # noqa: E402
|
|
20
|
+
import tilo.models.domain # noqa: F401, E402 — registers all ORM tables
|
|
21
|
+
|
|
22
|
+
target_metadata = Base.metadata
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_url() -> str:
|
|
26
|
+
from tilo.core.config import get_settings
|
|
27
|
+
return get_settings().database_url
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_migrations_offline() -> None:
|
|
31
|
+
"""Run migrations without a live DB connection (generates SQL script)."""
|
|
32
|
+
url = _get_url()
|
|
33
|
+
context.configure(
|
|
34
|
+
url=url,
|
|
35
|
+
target_metadata=target_metadata,
|
|
36
|
+
literal_binds=True,
|
|
37
|
+
dialect_opts={"paramstyle": "named"},
|
|
38
|
+
compare_type=True,
|
|
39
|
+
)
|
|
40
|
+
with context.begin_transaction():
|
|
41
|
+
context.run_migrations()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_migrations_online() -> None:
|
|
45
|
+
"""Run migrations against a live DB connection."""
|
|
46
|
+
cfg = dict(config.get_section(config.config_ini_section) or {})
|
|
47
|
+
cfg["sqlalchemy.url"] = _get_url()
|
|
48
|
+
connectable = engine_from_config(cfg, prefix="sqlalchemy.", poolclass=pool.NullPool)
|
|
49
|
+
with connectable.connect() as connection:
|
|
50
|
+
context.configure(
|
|
51
|
+
connection=connection,
|
|
52
|
+
target_metadata=target_metadata,
|
|
53
|
+
compare_type=True,
|
|
54
|
+
)
|
|
55
|
+
with context.begin_transaction():
|
|
56
|
+
context.run_migrations()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if context.is_offline_mode():
|
|
60
|
+
run_migrations_offline()
|
|
61
|
+
else:
|
|
62
|
+
run_migrations_online()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Initial schema baseline.
|
|
2
|
+
|
|
3
|
+
Revision ID: 0001
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-06-04
|
|
6
|
+
|
|
7
|
+
This migration is a **no-op baseline marker**.
|
|
8
|
+
|
|
9
|
+
On fresh databases, `Base.metadata.create_all()` creates all tables
|
|
10
|
+
before Alembic runs. The startup code then stamps this revision as
|
|
11
|
+
applied so subsequent migrations are tracked correctly.
|
|
12
|
+
|
|
13
|
+
On pre-Alembic databases (v0.1 installs), this migration records that
|
|
14
|
+
the initial schema is already in place, and migration 0002 handles the
|
|
15
|
+
additive v02 column changes.
|
|
16
|
+
"""
|
|
17
|
+
from alembic import op # noqa: F401
|
|
18
|
+
|
|
19
|
+
revision = "0001"
|
|
20
|
+
down_revision = None
|
|
21
|
+
branch_labels = None
|
|
22
|
+
depends_on = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def upgrade() -> None:
|
|
26
|
+
# Tables exist on all databases (either via create_all or pre-existing).
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def downgrade() -> None:
|
|
31
|
+
pass
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Add v02 schema columns and surface_turns table.
|
|
2
|
+
|
|
3
|
+
Revision ID: 0002
|
|
4
|
+
Revises: 0001
|
|
5
|
+
Create Date: 2026-06-04
|
|
6
|
+
|
|
7
|
+
Converts the manual `ensure_v02_schema()` patches into tracked Alembic
|
|
8
|
+
migrations. Each change is idempotent (checks before altering) so it is
|
|
9
|
+
safe to run against databases that already applied `ensure_v02_schema`.
|
|
10
|
+
|
|
11
|
+
Changes:
|
|
12
|
+
runs → add session_id (VARCHAR, nullable)
|
|
13
|
+
memories → add scope_type, scope_id, source_run_id, salience,
|
|
14
|
+
status, structured_payload, supersedes_id,
|
|
15
|
+
last_recalled_at, recall_count
|
|
16
|
+
surface_turns → create table if not present
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sqlalchemy as sa
|
|
21
|
+
from alembic import op
|
|
22
|
+
from sqlalchemy import inspect
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _column_exists(table: str, column: str) -> bool:
|
|
26
|
+
bind = op.get_bind()
|
|
27
|
+
insp = inspect(bind)
|
|
28
|
+
return column in {col["name"] for col in insp.get_columns(table)}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _add_if_missing(table: str, column: str, col_type: sa.types.TypeEngine, server_default: str | None = None) -> None:
|
|
32
|
+
if not _column_exists(table, column):
|
|
33
|
+
op.add_column(table, sa.Column(column, col_type, server_default=server_default, nullable=True))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
revision = "0002"
|
|
37
|
+
down_revision = "0001"
|
|
38
|
+
branch_labels = None
|
|
39
|
+
depends_on = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def upgrade() -> None:
|
|
43
|
+
bind = op.get_bind()
|
|
44
|
+
is_pg = bind.dialect.name == "postgresql"
|
|
45
|
+
datetime_type: sa.types.TypeEngine = sa.TIMESTAMP() if is_pg else sa.DateTime()
|
|
46
|
+
|
|
47
|
+
# -- runs ----------------------------------------------------------------
|
|
48
|
+
_add_if_missing("runs", "session_id", sa.String())
|
|
49
|
+
|
|
50
|
+
# -- memories ------------------------------------------------------------
|
|
51
|
+
_add_if_missing("memories", "scope_type", sa.String(), server_default="workspace")
|
|
52
|
+
_add_if_missing("memories", "scope_id", sa.String())
|
|
53
|
+
_add_if_missing("memories", "source_run_id", sa.String())
|
|
54
|
+
_add_if_missing("memories", "salience", sa.Float(), server_default="0.5")
|
|
55
|
+
_add_if_missing("memories", "status", sa.String(), server_default="candidate")
|
|
56
|
+
_add_if_missing("memories", "structured_payload", sa.JSON())
|
|
57
|
+
_add_if_missing("memories", "supersedes_id", sa.String())
|
|
58
|
+
_add_if_missing("memories", "last_recalled_at", datetime_type)
|
|
59
|
+
_add_if_missing("memories", "recall_count", sa.Integer(), server_default="0")
|
|
60
|
+
|
|
61
|
+
# Backfill status for rows that pre-date the column.
|
|
62
|
+
op.execute(
|
|
63
|
+
sa.text(
|
|
64
|
+
"UPDATE memories "
|
|
65
|
+
"SET status = CASE WHEN is_confirmed THEN 'confirmed' ELSE 'candidate' END "
|
|
66
|
+
"WHERE status IS NULL OR status = ''"
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
op.execute(sa.text("UPDATE memories SET salience = 0.5 WHERE salience IS NULL"))
|
|
70
|
+
op.execute(sa.text("UPDATE memories SET recall_count = 0 WHERE recall_count IS NULL"))
|
|
71
|
+
op.execute(
|
|
72
|
+
sa.text("UPDATE memories SET scope_type = 'workspace' WHERE scope_type IS NULL OR scope_type = ''")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# -- surface_turns -------------------------------------------------------
|
|
76
|
+
insp = inspect(bind)
|
|
77
|
+
if "surface_turns" not in insp.get_table_names():
|
|
78
|
+
op.create_table(
|
|
79
|
+
"surface_turns",
|
|
80
|
+
sa.Column("id", sa.String(), primary_key=True),
|
|
81
|
+
sa.Column("run_id", sa.String(), sa.ForeignKey("runs.id"), nullable=True, index=True),
|
|
82
|
+
sa.Column("session_id", sa.String(), sa.ForeignKey("conversation_sessions.id"), nullable=True, index=True),
|
|
83
|
+
sa.Column("workspace_id", sa.String(), nullable=True),
|
|
84
|
+
sa.Column("step_index", sa.Integer(), nullable=True),
|
|
85
|
+
sa.Column("intent", sa.String(), nullable=True),
|
|
86
|
+
sa.Column("spec_json", sa.JSON(), nullable=True),
|
|
87
|
+
sa.Column("budget_hint", sa.String(), nullable=True),
|
|
88
|
+
sa.Column("created_at", datetime_type, nullable=True),
|
|
89
|
+
sa.Column("updated_at", datetime_type, nullable=True),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def downgrade() -> None:
|
|
94
|
+
# Additive column migrations are not reversible in production.
|
|
95
|
+
# Drop surface_turns only (safe — it is the newest table).
|
|
96
|
+
bind = op.get_bind()
|
|
97
|
+
insp = inspect(bind)
|
|
98
|
+
if "surface_turns" in insp.get_table_names():
|
|
99
|
+
op.drop_table("surface_turns")
|
tilo-0.1.0/alembic.ini
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Alembic configuration for Tilo Framework.
|
|
2
|
+
# The database URL is intentionally blank — it is injected at runtime
|
|
3
|
+
# by alembic/env.py from tilo.core.config (respects DATABASE_URL env var).
|
|
4
|
+
|
|
5
|
+
[alembic]
|
|
6
|
+
script_location = alembic
|
|
7
|
+
prepend_sys_path = .
|
|
8
|
+
file_template = %%(rev)s_%%(slug)s
|
|
9
|
+
truncate_slug_length = 40
|
|
10
|
+
|
|
11
|
+
[loggers]
|
|
12
|
+
keys = root,sqlalchemy,alembic
|
|
13
|
+
|
|
14
|
+
[handlers]
|
|
15
|
+
keys = console
|
|
16
|
+
|
|
17
|
+
[formatters]
|
|
18
|
+
keys = generic
|
|
19
|
+
|
|
20
|
+
[logger_root]
|
|
21
|
+
level = WARN
|
|
22
|
+
handlers = console
|
|
23
|
+
qualname =
|
|
24
|
+
|
|
25
|
+
[logger_sqlalchemy]
|
|
26
|
+
level = WARN
|
|
27
|
+
handlers =
|
|
28
|
+
qualname = sqlalchemy.engine
|
|
29
|
+
|
|
30
|
+
[logger_alembic]
|
|
31
|
+
level = INFO
|
|
32
|
+
handlers =
|
|
33
|
+
qualname = alembic
|
|
34
|
+
|
|
35
|
+
[handler_console]
|
|
36
|
+
class = StreamHandler
|
|
37
|
+
args = (sys.stderr,)
|
|
38
|
+
level = NOTSET
|
|
39
|
+
formatter = generic
|
|
40
|
+
|
|
41
|
+
[formatter_generic]
|
|
42
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
43
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tilo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tilo — AI-native SaaS agent framework with long-term memory, structured artifacts, and human confirmation."
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
keywords = ["ai", "agent", "framework", "memory", "artifact", "saas"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Framework :: FastAPI",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"fastapi>=0.110",
|
|
22
|
+
"uvicorn[standard]>=0.27",
|
|
23
|
+
"sqlalchemy>=2.0",
|
|
24
|
+
"alembic>=1.13",
|
|
25
|
+
"pydantic>=2.6",
|
|
26
|
+
"pydantic-settings>=2.2",
|
|
27
|
+
"psycopg[binary]>=3.1",
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"PyYAML>=6.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
test = ["pytest>=8.0"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
# `pip install tilo` gives you:
|
|
37
|
+
# tilo serve — start the API server
|
|
38
|
+
# tilo init — scaffold a new Tilo project
|
|
39
|
+
tilo = "tilo.cli:main"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["tilo"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
db_path = Path(tempfile.gettempdir()) / "tilo_smoke_test.db"
|
|
11
|
+
if db_path.exists():
|
|
12
|
+
db_path.unlink()
|
|
13
|
+
|
|
14
|
+
os.environ["DATABASE_URL"] = f"sqlite:///{db_path}"
|
|
15
|
+
os.environ["LLM_ENABLED"] = "false"
|
|
16
|
+
os.environ["LLM_PROVIDER"] = "openai"
|
|
17
|
+
os.environ["LLM_BASE_URL"] = ""
|
|
18
|
+
os.environ["OPENAI_API_KEY"] = ""
|
|
19
|
+
|
|
20
|
+
from fastapi.testclient import TestClient # noqa: E402
|
|
21
|
+
|
|
22
|
+
from tilo.core.config import Settings # noqa: E402
|
|
23
|
+
from tilo.core.database import SessionLocal # noqa: E402
|
|
24
|
+
from tilo.main import app # noqa: E402
|
|
25
|
+
from tilo.models import Artifact, Confirmation, ContextReflection, ConversationSession, ConversationTurn, Memory, Run, Task, Tool, TraceStep, UIInteractionEvent # noqa: E402
|
|
26
|
+
from tilo.services.agent_context import AgentContextBuilder # noqa: E402
|
|
27
|
+
from tilo.services.agent_runtime.run_manager import RunManager # noqa: E402
|
|
28
|
+
from tilo.services.agent_runtime.prompt_builder import PromptBuilder # noqa: E402
|
|
29
|
+
from tilo.services.agent_runtime.state_machine import InvalidStateTransition, RunStateMachine # noqa: E402
|
|
30
|
+
from tilo.services.artifact.generator import ArtifactGenerator # noqa: E402
|
|
31
|
+
from tilo.services.artifact.spec import ArtifactSpecBuilder # noqa: E402
|
|
32
|
+
from tilo.services.channels.telegram.adapter import TelegramAdapter # noqa: E402
|
|
33
|
+
from tilo.services.channels.telegram.types import parse_telegram_callback_data # noqa: E402
|
|
34
|
+
from tilo.services.artifact.contract_llm import ContractReviewLLMGenerator # noqa: E402
|
|
35
|
+
from tilo.services.models.client import ModelClient # noqa: E402
|
|
36
|
+
from tilo.services.models.errors import ModelDisabledError, ModelInvalidJSONError # noqa: E402
|
|
37
|
+
from tilo.services.demo import load_problematic_ai_service_agreement # noqa: E402
|
|
38
|
+
from tilo.services.apps.loader import AgentAppLoader, get_app_loader # noqa: E402
|
|
39
|
+
from tilo.services.conversations.constants import ConversationChannel, ConversationTurnType # noqa: E402
|
|
40
|
+
from tilo.services.conversations.service import ConversationService # noqa: E402
|
|
41
|
+
from tilo.services.context_reflection import ContextReflectionService # noqa: E402
|
|
42
|
+
from tilo.services.interaction_policy.schemas import InteractionContext, InteractionDecisionType # noqa: E402
|
|
43
|
+
from tilo.services.interaction_policy.service import InteractionPolicyService # noqa: E402
|
|
44
|
+
from tilo.schemas import RichSurfaceLink # noqa: E402
|
|
45
|
+
from tilo.schemas.artifact import CORE_BLOCK_TYPES, PRIMITIVE_BLOCK_TYPES, ArtifactSpecV1 # noqa: E402
|
|
46
|
+
from tilo.services.surfaces.constants import RichSurfaceSource, RichSurfaceTargetType # noqa: E402
|
|
47
|
+
from tilo.services.surfaces.rich_links import create_rich_surface_link # noqa: E402
|
|
48
|
+
from tilo.services.trace.recorder import TraceRecorder # noqa: E402
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_action_artifact(
|
|
52
|
+
*,
|
|
53
|
+
workspace_id: str,
|
|
54
|
+
actions: list[dict] | None = None,
|
|
55
|
+
blocks: list[dict] | None = None,
|
|
56
|
+
run_id: str | None = None,
|
|
57
|
+
) -> str:
|
|
58
|
+
with SessionLocal() as db:
|
|
59
|
+
artifact = Artifact(
|
|
60
|
+
workspace_id=workspace_id,
|
|
61
|
+
run_id=run_id,
|
|
62
|
+
title="Action Runtime Artifact",
|
|
63
|
+
type="contract_review",
|
|
64
|
+
schema_json={
|
|
65
|
+
"version": "artifact_spec.v1",
|
|
66
|
+
"artifact_type": "contract_review",
|
|
67
|
+
"title": "Action Runtime Artifact",
|
|
68
|
+
"status": "ready",
|
|
69
|
+
"blocks": blocks
|
|
70
|
+
or [
|
|
71
|
+
{
|
|
72
|
+
"id": "summary",
|
|
73
|
+
"type": "approval_card",
|
|
74
|
+
"data": {"title": "Approve"},
|
|
75
|
+
"actions": [
|
|
76
|
+
{
|
|
77
|
+
"id": "approve_block",
|
|
78
|
+
"label": "Approve block",
|
|
79
|
+
"action_type": "approve",
|
|
80
|
+
"confirmation_required": False,
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"actions": actions or [],
|
|
86
|
+
"run_id": run_id,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
db.add(artifact)
|
|
90
|
+
db.commit()
|
|
91
|
+
db.refresh(artifact)
|
|
92
|
+
return artifact.id
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tests for the AIP spec generator and skill hint system."""
|
|
2
|
+
from helpers import * # noqa: F401,F403
|
|
3
|
+
|
|
4
|
+
from tilo.services.artifact.aip_generator import (
|
|
5
|
+
AIPSpecGenerator,
|
|
6
|
+
ArtifactTypeDetector,
|
|
7
|
+
_detect_skill_hint,
|
|
8
|
+
_DEMO_SKILL_HINTS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_skill_hint_detection_contract() -> None:
|
|
13
|
+
hint = _detect_skill_hint("Review this contract for risky clauses")
|
|
14
|
+
assert hint == _DEMO_SKILL_HINTS["contract_review"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_skill_hint_detection_sales() -> None:
|
|
18
|
+
hint = _detect_skill_hint("Follow up with my top customers")
|
|
19
|
+
assert hint == _DEMO_SKILL_HINTS["sales_dashboard"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_skill_hint_detection_code_review() -> None:
|
|
23
|
+
hint = _detect_skill_hint("Review this pull request from the auth refactor branch")
|
|
24
|
+
assert hint == _DEMO_SKILL_HINTS["code_review"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_skill_hint_detection_generic() -> None:
|
|
28
|
+
hint = _detect_skill_hint("Write me a poem about cats")
|
|
29
|
+
assert hint is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_artifact_type_detector() -> None:
|
|
33
|
+
d = ArtifactTypeDetector()
|
|
34
|
+
assert d.detect("review this contract") == "contract_review"
|
|
35
|
+
assert d.detect("follow up with sales leads") == "dashboard"
|
|
36
|
+
assert d.detect("review this pull request") == "code_review"
|
|
37
|
+
assert d.detect("help me write a report") == "document"
|
|
38
|
+
assert d.detect("审查这份合同") == "contract_review"
|
|
39
|
+
assert d.detect("代码评审一下这个 PR") == "code_review"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_aip_deterministic_fallback() -> None:
|
|
43
|
+
"""AIPSpecGenerator without a model client should produce a valid deterministic spec."""
|
|
44
|
+
task = Task(id="t1", workspace_id="ws", title="Test", input_message="Hello world")
|
|
45
|
+
run = Run(id="r1", task_id="t1")
|
|
46
|
+
gen = AIPSpecGenerator(client=None)
|
|
47
|
+
result = gen.generate(task, run, [], [])
|
|
48
|
+
assert result["version"] == "tilo/aip/v1"
|
|
49
|
+
assert result["_generation_mode"] == "deterministic"
|
|
50
|
+
assert len(result["blocks"]) >= 1
|
|
51
|
+
spec = ArtifactSpecV1.model_validate(result)
|
|
52
|
+
assert spec.title
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_aip_deterministic_fallback_chinese() -> None:
|
|
56
|
+
"""Even with Chinese input, the demo fallback now returns English titles
|
|
57
|
+
so the Canvas demo stays consistent. The hint detector still triggers
|
|
58
|
+
contract scenario via Chinese keywords."""
|
|
59
|
+
task = Task(id="t2", workspace_id="ws", title="Test", input_message="审查这份合同")
|
|
60
|
+
run = Run(id="r2", task_id="t2")
|
|
61
|
+
gen = AIPSpecGenerator(client=None)
|
|
62
|
+
result = gen.generate(task, run, [], [])
|
|
63
|
+
assert result["title"] == "Contract Review"
|
|
64
|
+
assert len(result["blocks"]) >= 5 # Rich multi-block output
|