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.
Files changed (135) hide show
  1. tilo-0.1.0/.gitignore +40 -0
  2. tilo-0.1.0/Dockerfile +12 -0
  3. tilo-0.1.0/PKG-INFO +24 -0
  4. tilo-0.1.0/alembic/__init__.py +0 -0
  5. tilo-0.1.0/alembic/env.py +62 -0
  6. tilo-0.1.0/alembic/versions/0001_initial_schema.py +31 -0
  7. tilo-0.1.0/alembic/versions/0002_v02_columns.py +99 -0
  8. tilo-0.1.0/alembic.ini +43 -0
  9. tilo-0.1.0/pyproject.toml +45 -0
  10. tilo-0.1.0/tests/helpers.py +92 -0
  11. tilo-0.1.0/tests/test_aip_generator.py +64 -0
  12. tilo-0.1.0/tests/test_apps_and_policy.py +342 -0
  13. tilo-0.1.0/tests/test_artifact_action_runtime.py +329 -0
  14. tilo-0.1.0/tests/test_artifact_spec.py +87 -0
  15. tilo-0.1.0/tests/test_behaviour_memory.py +383 -0
  16. tilo-0.1.0/tests/test_conversation_runtime.py +282 -0
  17. tilo-0.1.0/tests/test_demo_contract.py +391 -0
  18. tilo-0.1.0/tests/test_health_and_runtime.py +122 -0
  19. tilo-0.1.0/tests/test_langchain_adapter.py +237 -0
  20. tilo-0.1.0/tests/test_mcp_adapter.py +62 -0
  21. tilo-0.1.0/tests/test_memory_lifecycle.py +53 -0
  22. tilo-0.1.0/tests/test_model_runtime.py +83 -0
  23. tilo-0.1.0/tests/test_policy_runtime_integration.py +395 -0
  24. tilo-0.1.0/tests/test_run_streaming.py +406 -0
  25. tilo-0.1.0/tests/test_surface_schema.py +635 -0
  26. tilo-0.1.0/tilo/__init__.py +1 -0
  27. tilo-0.1.0/tilo/adapters/__init__.py +5 -0
  28. tilo-0.1.0/tilo/adapters/a2a.py +29 -0
  29. tilo-0.1.0/tilo/adapters/acp.py +29 -0
  30. tilo-0.1.0/tilo/adapters/langchain.py +347 -0
  31. tilo-0.1.0/tilo/adapters/mcp.py +125 -0
  32. tilo-0.1.0/tilo/api/__init__.py +1 -0
  33. tilo-0.1.0/tilo/api/deps.py +20 -0
  34. tilo-0.1.0/tilo/api/routes/__init__.py +22 -0
  35. tilo-0.1.0/tilo/api/routes/agents.py +40 -0
  36. tilo-0.1.0/tilo/api/routes/apps.py +55 -0
  37. tilo-0.1.0/tilo/api/routes/artifacts.py +83 -0
  38. tilo-0.1.0/tilo/api/routes/channels.py +25 -0
  39. tilo-0.1.0/tilo/api/routes/confirmations.py +57 -0
  40. tilo-0.1.0/tilo/api/routes/conversations.py +142 -0
  41. tilo-0.1.0/tilo/api/routes/demo.py +19 -0
  42. tilo-0.1.0/tilo/api/routes/feedback.py +34 -0
  43. tilo-0.1.0/tilo/api/routes/interactions.py +58 -0
  44. tilo-0.1.0/tilo/api/routes/memories.py +146 -0
  45. tilo-0.1.0/tilo/api/routes/messages.py +52 -0
  46. tilo-0.1.0/tilo/api/routes/projects.py +40 -0
  47. tilo-0.1.0/tilo/api/routes/runs.py +42 -0
  48. tilo-0.1.0/tilo/api/routes/skills.py +104 -0
  49. tilo-0.1.0/tilo/api/routes/system.py +44 -0
  50. tilo-0.1.0/tilo/api/routes/tasks.py +54 -0
  51. tilo-0.1.0/tilo/api/routes/tools.py +67 -0
  52. tilo-0.1.0/tilo/api/routes/workspaces.py +40 -0
  53. tilo-0.1.0/tilo/cli.py +107 -0
  54. tilo-0.1.0/tilo/core/__init__.py +1 -0
  55. tilo-0.1.0/tilo/core/config.py +46 -0
  56. tilo-0.1.0/tilo/core/database.py +24 -0
  57. tilo-0.1.0/tilo/core/migrations.py +67 -0
  58. tilo-0.1.0/tilo/core/time.py +5 -0
  59. tilo-0.1.0/tilo/main.py +93 -0
  60. tilo-0.1.0/tilo/models/__init__.py +53 -0
  61. tilo-0.1.0/tilo/models/domain.py +404 -0
  62. tilo-0.1.0/tilo/schemas/__init__.py +3 -0
  63. tilo-0.1.0/tilo/schemas/artifact.py +241 -0
  64. tilo-0.1.0/tilo/schemas/domain.py +533 -0
  65. tilo-0.1.0/tilo/schemas/surface.py +504 -0
  66. tilo-0.1.0/tilo/services/__init__.py +1 -0
  67. tilo-0.1.0/tilo/services/agent_context/__init__.py +3 -0
  68. tilo-0.1.0/tilo/services/agent_context/builder.py +127 -0
  69. tilo-0.1.0/tilo/services/agent_runtime/__init__.py +3 -0
  70. tilo-0.1.0/tilo/services/agent_runtime/executor.py +38 -0
  71. tilo-0.1.0/tilo/services/agent_runtime/message_flow.py +116 -0
  72. tilo-0.1.0/tilo/services/agent_runtime/planner.py +99 -0
  73. tilo-0.1.0/tilo/services/agent_runtime/prompt_builder.py +72 -0
  74. tilo-0.1.0/tilo/services/agent_runtime/run_manager.py +457 -0
  75. tilo-0.1.0/tilo/services/agent_runtime/state_machine.py +87 -0
  76. tilo-0.1.0/tilo/services/apps/__init__.py +4 -0
  77. tilo-0.1.0/tilo/services/apps/loader.py +88 -0
  78. tilo-0.1.0/tilo/services/apps/schemas.py +48 -0
  79. tilo-0.1.0/tilo/services/artifact/__init__.py +3 -0
  80. tilo-0.1.0/tilo/services/artifact/actions.py +523 -0
  81. tilo-0.1.0/tilo/services/artifact/aip_generator.py +887 -0
  82. tilo-0.1.0/tilo/services/artifact/contract_llm.py +114 -0
  83. tilo-0.1.0/tilo/services/artifact/generator.py +296 -0
  84. tilo-0.1.0/tilo/services/artifact/persistence.py +44 -0
  85. tilo-0.1.0/tilo/services/artifact/spec.py +923 -0
  86. tilo-0.1.0/tilo/services/bootstrap.py +50 -0
  87. tilo-0.1.0/tilo/services/channels/__init__.py +2 -0
  88. tilo-0.1.0/tilo/services/channels/telegram/__init__.py +4 -0
  89. tilo-0.1.0/tilo/services/channels/telegram/adapter.py +81 -0
  90. tilo-0.1.0/tilo/services/channels/telegram/renderer.py +59 -0
  91. tilo-0.1.0/tilo/services/channels/telegram/types.py +18 -0
  92. tilo-0.1.0/tilo/services/channels/telegram/webhook.py +259 -0
  93. tilo-0.1.0/tilo/services/channels/types.py +64 -0
  94. tilo-0.1.0/tilo/services/context_reflection/__init__.py +11 -0
  95. tilo-0.1.0/tilo/services/context_reflection/schemas.py +31 -0
  96. tilo-0.1.0/tilo/services/context_reflection/service.py +224 -0
  97. tilo-0.1.0/tilo/services/conversations/__init__.py +3 -0
  98. tilo-0.1.0/tilo/services/conversations/constants.py +25 -0
  99. tilo-0.1.0/tilo/services/conversations/messages.py +121 -0
  100. tilo-0.1.0/tilo/services/conversations/service.py +190 -0
  101. tilo-0.1.0/tilo/services/demo/__init__.py +6 -0
  102. tilo-0.1.0/tilo/services/demo/contracts.py +41 -0
  103. tilo-0.1.0/tilo/services/improvement/__init__.py +4 -0
  104. tilo-0.1.0/tilo/services/improvement/candidates.py +144 -0
  105. tilo-0.1.0/tilo/services/improvement/metrics.py +50 -0
  106. tilo-0.1.0/tilo/services/inbox/__init__.py +3 -0
  107. tilo-0.1.0/tilo/services/inbox/confirmations.py +77 -0
  108. tilo-0.1.0/tilo/services/interaction_policy/__init__.py +3 -0
  109. tilo-0.1.0/tilo/services/interaction_policy/schemas.py +125 -0
  110. tilo-0.1.0/tilo/services/interaction_policy/service.py +161 -0
  111. tilo-0.1.0/tilo/services/interactions/__init__.py +1 -0
  112. tilo-0.1.0/tilo/services/interactions/events.py +51 -0
  113. tilo-0.1.0/tilo/services/memory/__init__.py +4 -0
  114. tilo-0.1.0/tilo/services/memory/behaviour.py +320 -0
  115. tilo-0.1.0/tilo/services/memory/extraction.py +124 -0
  116. tilo-0.1.0/tilo/services/memory/recall.py +209 -0
  117. tilo-0.1.0/tilo/services/memory/writer.py +160 -0
  118. tilo-0.1.0/tilo/services/models/__init__.py +11 -0
  119. tilo-0.1.0/tilo/services/models/client.py +416 -0
  120. tilo-0.1.0/tilo/services/models/errors.py +18 -0
  121. tilo-0.1.0/tilo/services/models/prompts.py +105 -0
  122. tilo-0.1.0/tilo/services/models/schemas.py +129 -0
  123. tilo-0.1.0/tilo/services/skill/__init__.py +3 -0
  124. tilo-0.1.0/tilo/services/skill/selector.py +27 -0
  125. tilo-0.1.0/tilo/services/surface/__init__.py +23 -0
  126. tilo-0.1.0/tilo/services/surface/composer.py +519 -0
  127. tilo-0.1.0/tilo/services/surface/persistence.py +145 -0
  128. tilo-0.1.0/tilo/services/surfaces/__init__.py +3 -0
  129. tilo-0.1.0/tilo/services/surfaces/constants.py +13 -0
  130. tilo-0.1.0/tilo/services/surfaces/rich_links.py +31 -0
  131. tilo-0.1.0/tilo/services/tools/__init__.py +3 -0
  132. tilo-0.1.0/tilo/services/tools/invocation.py +129 -0
  133. tilo-0.1.0/tilo/services/tools/registry.py +40 -0
  134. tilo-0.1.0/tilo/services/trace/__init__.py +3 -0
  135. 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