caudate-cli 0.1.0__py3-none-any.whl

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 (153) hide show
  1. api/__init__.py +5 -0
  2. api/anthropic_compat.py +1518 -0
  3. api/artifact_viewer.py +366 -0
  4. api/caudate_middleware.py +618 -0
  5. api/forge_bootstrapper_routes.py +377 -0
  6. api/forge_routes.py +630 -0
  7. api/forge_system_routes.py +294 -0
  8. api/openai_compat.py +1993 -0
  9. api/server.py +667 -0
  10. api/storyboard_page.py +677 -0
  11. caudate_cli-0.1.0.dist-info/METADATA +354 -0
  12. caudate_cli-0.1.0.dist-info/RECORD +153 -0
  13. caudate_cli-0.1.0.dist-info/WHEEL +5 -0
  14. caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
  15. caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  16. caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
  17. cognos_mcp/__init__.py +4 -0
  18. cognos_mcp/bridge.py +41 -0
  19. cognos_mcp/client.py +70 -0
  20. cognos_mcp/config.py +49 -0
  21. cognos_mcp/server.py +66 -0
  22. config.py +82 -0
  23. core/__init__.py +0 -0
  24. core/agent.py +468 -0
  25. core/agentic_loop.py +731 -0
  26. core/anthropic_auth.py +91 -0
  27. core/background.py +113 -0
  28. core/banner.py +134 -0
  29. core/bootstrap.py +292 -0
  30. core/citations.py +131 -0
  31. core/compaction.py +109 -0
  32. core/constitution.py +198 -0
  33. core/diff_viewer.py +87 -0
  34. core/export.py +85 -0
  35. core/file_refs.py +119 -0
  36. core/files.py +199 -0
  37. core/hooks.py +209 -0
  38. core/image.py +599 -0
  39. core/input.py +91 -0
  40. core/loop.py +238 -0
  41. core/memory_md.py +147 -0
  42. core/notifications.py +99 -0
  43. core/ownership.py +181 -0
  44. core/paste.py +81 -0
  45. core/permissions.py +210 -0
  46. core/plan_mode.py +215 -0
  47. core/sandbox_prompt.py +185 -0
  48. core/scheduler.py +195 -0
  49. core/schemas.py +202 -0
  50. core/session.py +90 -0
  51. core/settings.py +132 -0
  52. core/skills.py +398 -0
  53. core/slash_commands.py +977 -0
  54. core/statusline.py +61 -0
  55. core/subagent.py +300 -0
  56. core/thinking.py +50 -0
  57. core/updater.py +122 -0
  58. core/usage.py +109 -0
  59. core/worktree.py +93 -0
  60. execution/__init__.py +0 -0
  61. execution/executor.py +329 -0
  62. execution/plugins.py +108 -0
  63. execution/tools/__init__.py +0 -0
  64. execution/tools/agent_tool.py +107 -0
  65. execution/tools/agentic_tool.py +297 -0
  66. execution/tools/artifact_tool.py +191 -0
  67. execution/tools/ask_user_question_tool.py +137 -0
  68. execution/tools/base.py +81 -0
  69. execution/tools/calculator_tool.py +137 -0
  70. execution/tools/cognos_card_tool.py +124 -0
  71. execution/tools/cron_tool.py +215 -0
  72. execution/tools/datetime_tool.py +215 -0
  73. execution/tools/describe_image_tool.py +161 -0
  74. execution/tools/draw_tool.py +164 -0
  75. execution/tools/edit_image_tool.py +262 -0
  76. execution/tools/edit_tool.py +245 -0
  77. execution/tools/file_tool.py +90 -0
  78. execution/tools/find_anywhere_tool.py +255 -0
  79. execution/tools/forge_feature_tools.py +377 -0
  80. execution/tools/glob_tool.py +59 -0
  81. execution/tools/grep_tool.py +89 -0
  82. execution/tools/http_request_tool.py +224 -0
  83. execution/tools/load_skill_tool.py +104 -0
  84. execution/tools/longcat_avatar_tool.py +384 -0
  85. execution/tools/mcp_tool.py +100 -0
  86. execution/tools/notebook_tool.py +279 -0
  87. execution/tools/openapi_tool.py +440 -0
  88. execution/tools/plan_mode_tool.py +95 -0
  89. execution/tools/push_notification_tool.py +157 -0
  90. execution/tools/python_tool.py +61 -0
  91. execution/tools/respond_tool.py +40 -0
  92. execution/tools/sandbox_tool.py +378 -0
  93. execution/tools/search_tool.py +153 -0
  94. execution/tools/semantic_search_tool.py +106 -0
  95. execution/tools/shell_tool.py +283 -0
  96. execution/tools/speak_tool.py +134 -0
  97. execution/tools/storyboard_tool.py +727 -0
  98. execution/tools/system_info_tool.py +212 -0
  99. execution/tools/task_tool.py +323 -0
  100. execution/tools/think_tool.py +49 -0
  101. execution/tools/transcribe_audio_tool.py +86 -0
  102. execution/tools/update_memory_tool.py +92 -0
  103. execution/tools/web_fetch_tool.py +82 -0
  104. execution/tools/worktree_tool.py +174 -0
  105. llm/__init__.py +0 -0
  106. llm/fallback.py +116 -0
  107. llm/models.py +320 -0
  108. llm/provider.py +1356 -0
  109. llm/router.py +373 -0
  110. main.py +1889 -0
  111. memory/__init__.py +0 -0
  112. memory/episodic.py +99 -0
  113. memory/procedural.py +145 -0
  114. memory/semantic.py +71 -0
  115. memory/working.py +64 -0
  116. nn/__init__.py +43 -0
  117. nn/auto_evolve.py +245 -0
  118. nn/caudate.py +136 -0
  119. nn/config.py +141 -0
  120. nn/consolidator.py +81 -0
  121. nn/data.py +1635 -0
  122. nn/encoder.py +258 -0
  123. nn/forge_advisor.py +303 -0
  124. nn/format.py +235 -0
  125. nn/heads.py +432 -0
  126. nn/observer.py +994 -0
  127. nn/policy.py +214 -0
  128. nn/runtime.py +343 -0
  129. nn/scorer.py +175 -0
  130. nn/trainer.py +515 -0
  131. nn/vision.py +352 -0
  132. personality/__init__.py +23 -0
  133. personality/engine.py +129 -0
  134. personality/identity.py +144 -0
  135. personality/inner_voice.py +100 -0
  136. personality/mood.py +205 -0
  137. planning/__init__.py +0 -0
  138. planning/dev_server.py +221 -0
  139. planning/forge_models.py +718 -0
  140. planning/orchestrator.py +1363 -0
  141. planning/planner.py +451 -0
  142. planning/task_graph.py +61 -0
  143. reflection/__init__.py +0 -0
  144. reflection/meta_learner.py +156 -0
  145. reflection/reflector.py +127 -0
  146. ui/__init__.py +5 -0
  147. ui/display.py +88 -0
  148. voice/__init__.py +0 -0
  149. voice/conversation.py +125 -0
  150. voice/listener.py +111 -0
  151. voice/speaker.py +59 -0
  152. voice/stt.py +126 -0
  153. voice/tts.py +214 -0
@@ -0,0 +1,377 @@
1
+ """Bootstrapper chat-session routes — back-and-forth before committing.
2
+
3
+ Mirrors LocalForge's
4
+ GET/POST /api/projects/:id/bootstrapper-session
5
+ GET/POST /api/agent-sessions/:id/messages
6
+ POST /api/agent-sessions/:id/generate-features
7
+ POST /api/agent-sessions/:id/close
8
+
9
+ In Cognos, we reuse:
10
+ - ``forge_sessions`` rows where ``session_type="bootstrapper"``
11
+ - ``forge_chat_messages`` for the conversation
12
+ - ``LLMProvider.stream`` for the SSE response
13
+ - ``ForgeFeatureTools`` for agent-driven feature creation in
14
+ /generate-features (the agent already has these tools registered;
15
+ we just bind the project context with ``forge_project_scope``).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+ from datetime import datetime
24
+ from typing import Any, AsyncIterator
25
+
26
+ from fastapi import APIRouter, HTTPException
27
+ from fastapi.responses import StreamingResponse
28
+ from pydantic import BaseModel
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # Steering prompt for the conversational stage.
34
+ BOOTSTRAPPER_CHAT_SYSTEM_PROMPT = (
35
+ "You are Cognos Forge's AI bootstrapper. You help the user describe an "
36
+ "app they want to build. You are friendly, concise, and speak in 2-4 "
37
+ "sentences per reply. Ask one or two follow-up questions at a time "
38
+ "about the app's users, core features, data, and UI. When the user "
39
+ "seems satisfied with the plan, say so and invite them to click "
40
+ "'Generate feature list' to commit. Never produce code, markdown "
41
+ "tables, or long bullet walls — keep replies conversational."
42
+ )
43
+
44
+ # Steering prompt for the structured-output commit stage.
45
+ FEATURE_GEN_SYSTEM_PROMPT = (
46
+ "You are Cognos Forge's feature-generation agent.\n\n"
47
+ "You are given a user/assistant chat describing an app. Your job is "
48
+ "to turn that conversation into a complete backlog of 6-15 features "
49
+ "by calling the provided forge_* tools.\n\n"
50
+ "Workflow:\n"
51
+ "1. Call forge_list_features to see what (if anything) is already "
52
+ "in the backlog.\n"
53
+ "2. Plan the build in dependency order: foundational work first "
54
+ "(schema, app shell, core data model), then behaviour, then polish.\n"
55
+ "3. Call forge_create_feature for each item, in order. Pass "
56
+ "depends_on with the ids of earlier features that must complete "
57
+ "first. Depends_on must ONLY reference features that already exist.\n"
58
+ "4. Make sure the backlog covers persistence, UI, and at least one "
59
+ "polish/style feature.\n"
60
+ "5. When you are done creating features, STOP calling tools and "
61
+ "reply with a single short sentence summarising how many features "
62
+ "you created.\n\n"
63
+ "Rules:\n"
64
+ "- Title: short imperative sentence under 100 characters.\n"
65
+ "- Description: one paragraph describing what 'done' looks like.\n"
66
+ "- category: 'functional' or 'style' (default functional).\n\n"
67
+ "Do NOT output code, markdown fences, or JSON in your replies — "
68
+ "all structured output goes through the tools."
69
+ )
70
+
71
+
72
+ class ChatMessageIn(BaseModel):
73
+ content: str
74
+
75
+
76
+ class CloseSessionIn(BaseModel):
77
+ status: str | None = None # completed | failed | terminated
78
+
79
+
80
+ # ─────────────────────────── Helpers ────────────────────────────────
81
+
82
+
83
+ def _lazy():
84
+ from planning import forge_models as fm
85
+ return fm
86
+
87
+
88
+ def _get_active_bootstrapper(project_id: int) -> dict[str, Any] | None:
89
+ fm = _lazy()
90
+ with fm.session_scope() as sess:
91
+ row = (
92
+ sess.query(fm.ForgeSession)
93
+ .filter_by(
94
+ project_id=project_id,
95
+ session_type="bootstrapper",
96
+ status="in_progress",
97
+ )
98
+ .order_by(fm.ForgeSession.id.desc())
99
+ .first()
100
+ )
101
+ if row is None:
102
+ return None
103
+ return _session_dict(row)
104
+
105
+
106
+ def _session_dict(row) -> dict[str, Any]:
107
+ return {
108
+ "id": row.id,
109
+ "project_id": row.project_id,
110
+ "feature_id": row.feature_id,
111
+ "session_type": row.session_type,
112
+ "status": row.status,
113
+ "started_at": row.started_at.isoformat() if row.started_at else None,
114
+ "ended_at": row.ended_at.isoformat() if row.ended_at else None,
115
+ }
116
+
117
+
118
+ def _list_messages(session_id: int) -> list[dict[str, Any]]:
119
+ fm = _lazy()
120
+ with fm.session_scope() as sess:
121
+ rows = (
122
+ sess.query(fm.ForgeChatMessage)
123
+ .filter_by(session_id=session_id)
124
+ .order_by(fm.ForgeChatMessage.id.asc())
125
+ .all()
126
+ )
127
+ return [
128
+ {
129
+ "id": r.id,
130
+ "session_id": r.session_id,
131
+ "role": r.role,
132
+ "content": r.content,
133
+ "created_at": r.created_at.isoformat() if r.created_at else None,
134
+ }
135
+ for r in rows
136
+ ]
137
+
138
+
139
+ def _append_message(
140
+ session_id: int, role: str, content: str,
141
+ ) -> dict[str, Any]:
142
+ fm = _lazy()
143
+ with fm.session_scope() as sess:
144
+ row = fm.ForgeChatMessage(
145
+ session_id=session_id, role=role, content=content,
146
+ )
147
+ sess.add(row)
148
+ sess.flush()
149
+ return {
150
+ "id": row.id, "session_id": row.session_id,
151
+ "role": row.role, "content": row.content,
152
+ "created_at": row.created_at.isoformat() if row.created_at else None,
153
+ }
154
+
155
+
156
+ def _close_session(session_id: int, status: str) -> dict[str, Any] | None:
157
+ fm = _lazy()
158
+ valid = {"completed", "failed", "terminated"}
159
+ if status not in valid:
160
+ status = "completed"
161
+ with fm.session_scope() as sess:
162
+ row = sess.get(fm.ForgeSession, session_id)
163
+ if row is None:
164
+ return None
165
+ if row.status == "in_progress":
166
+ row.status = status
167
+ row.ended_at = datetime.utcnow()
168
+ return _session_dict(row)
169
+
170
+
171
+ # ─────────────────────────── Router ─────────────────────────────────
172
+
173
+
174
+ def build_router() -> APIRouter:
175
+ router = APIRouter(prefix="/forge", tags=["forge-bootstrapper"])
176
+
177
+ # ── Session create / fetch ──────────────────────────────────────
178
+
179
+ @router.get("/projects/{project_id}/bootstrapper-session")
180
+ async def get_bootstrapper(project_id: int) -> dict[str, Any]:
181
+ fm = _lazy()
182
+ if fm.get_project(project_id) is None:
183
+ raise HTTPException(404, "project not found")
184
+ return {"session": _get_active_bootstrapper(project_id)}
185
+
186
+ @router.post("/projects/{project_id}/bootstrapper-session")
187
+ async def post_bootstrapper(project_id: int) -> dict[str, Any]:
188
+ fm = _lazy()
189
+ if fm.get_project(project_id) is None:
190
+ raise HTTPException(404, "project not found")
191
+ existing = _get_active_bootstrapper(project_id)
192
+ if existing is not None:
193
+ return {"session": existing, "reused": True}
194
+ with fm.session_scope() as sess:
195
+ row = fm.ForgeSession(
196
+ project_id=project_id,
197
+ session_type="bootstrapper",
198
+ status="in_progress",
199
+ )
200
+ sess.add(row)
201
+ sess.flush()
202
+ return {"session": _session_dict(row), "reused": False}
203
+
204
+ # ── Messages ────────────────────────────────────────────────────
205
+
206
+ @router.get("/agent-sessions/{session_id}/messages")
207
+ async def list_messages(session_id: int) -> dict[str, Any]:
208
+ fm = _lazy()
209
+ with fm.session_scope() as sess:
210
+ row = sess.get(fm.ForgeSession, session_id)
211
+ if row is None:
212
+ raise HTTPException(404, "session not found")
213
+ return {"messages": _list_messages(session_id)}
214
+
215
+ @router.post("/agent-sessions/{session_id}/messages")
216
+ async def post_message(
217
+ session_id: int, body: ChatMessageIn,
218
+ ) -> StreamingResponse:
219
+ fm = _lazy()
220
+ with fm.session_scope() as sess:
221
+ srow = sess.get(fm.ForgeSession, session_id)
222
+ if srow is None:
223
+ raise HTTPException(404, "session not found")
224
+ if srow.status != "in_progress":
225
+ raise HTTPException(
226
+ 409, "this session is closed; start a new conversation",
227
+ )
228
+ project_id = srow.project_id
229
+
230
+ content = (body.content or "").strip()
231
+ if not content:
232
+ raise HTTPException(400, "field 'content' is required")
233
+
234
+ # Persist user message immediately so it survives downstream errors.
235
+ user_msg = _append_message(session_id, "user", content)
236
+ history = _list_messages(session_id)
237
+
238
+ async def gen() -> AsyncIterator[bytes]:
239
+ yield f"data: {json.dumps({'type': 'user', 'message': user_msg})}\n\n".encode()
240
+ from llm.provider import LLMProvider
241
+ from core.settings import Settings
242
+ from config import SYSTEM_1_MODEL, SYSTEM_2_MODEL, LLM_MODEL
243
+ from core.anthropic_auth import subscription_auth_scope
244
+
245
+ s = Settings.load()
246
+ chosen = (
247
+ s.get("system1") or SYSTEM_1_MODEL
248
+ or s.get("system2") or SYSTEM_2_MODEL
249
+ or LLM_MODEL
250
+ )
251
+ llm = LLMProvider(model=chosen)
252
+ messages = [
253
+ {"role": "system", "content": BOOTSTRAPPER_CHAT_SYSTEM_PROMPT},
254
+ ] + [
255
+ {"role": m["role"], "content": m["content"]}
256
+ for m in history
257
+ ]
258
+ full = ""
259
+ errored = False
260
+ try:
261
+ with subscription_auth_scope():
262
+ async for ev in llm.stream(messages):
263
+ et = getattr(ev, "type", None)
264
+ if et == "text_delta":
265
+ # core.schemas.StreamEvent uses .delta for text
266
+ delta = (
267
+ getattr(ev, "delta", None)
268
+ or getattr(ev, "text", "")
269
+ or ""
270
+ )
271
+ if delta:
272
+ full += delta
273
+ yield (
274
+ f"data: {json.dumps({'type': 'delta', 'content': delta})}\n\n"
275
+ ).encode()
276
+ elif et in ("message_stop", "message_end"):
277
+ break
278
+ except asyncio.CancelledError:
279
+ raise
280
+ except Exception as e:
281
+ errored = True
282
+ yield (
283
+ f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
284
+ ).encode()
285
+
286
+ if not errored:
287
+ assistant_msg = _append_message(
288
+ session_id, "assistant",
289
+ full.strip() or "(empty response)",
290
+ )
291
+ yield (
292
+ f"data: {json.dumps({'type': 'assistant', 'message': assistant_msg})}\n\n"
293
+ ).encode()
294
+ yield b'data: {"type": "done"}\n\n'
295
+
296
+ return StreamingResponse(gen(), media_type="text/event-stream")
297
+
298
+ # ── Close session ───────────────────────────────────────────────
299
+
300
+ @router.post("/agent-sessions/{session_id}/close")
301
+ async def close_session(
302
+ session_id: int, body: CloseSessionIn | None = None,
303
+ ) -> dict[str, Any]:
304
+ fm = _lazy()
305
+ with fm.session_scope() as sess:
306
+ row = sess.get(fm.ForgeSession, session_id)
307
+ if row is None:
308
+ raise HTTPException(404, "session not found")
309
+ if row.status != "in_progress":
310
+ return {"session": _session_dict(row), "closed": False}
311
+ status = (body.status if body else None) or "completed"
312
+ updated = _close_session(session_id, status)
313
+ return {"session": updated, "closed": True}
314
+
315
+ # ── Generate features (commit transcript → backlog) ─────────────
316
+
317
+ @router.post("/agent-sessions/{session_id}/generate-features")
318
+ async def generate_features(session_id: int) -> dict[str, Any]:
319
+ fm = _lazy()
320
+ with fm.session_scope() as sess:
321
+ srow = sess.get(fm.ForgeSession, session_id)
322
+ if srow is None:
323
+ raise HTTPException(404, "session not found")
324
+ if srow.session_type != "bootstrapper":
325
+ raise HTTPException(400, "not a bootstrapper session")
326
+ project_id = srow.project_id
327
+ history = _list_messages(session_id)
328
+ if not history:
329
+ raise HTTPException(
330
+ 400, "no conversation yet — send a message first",
331
+ )
332
+
333
+ # Prefix the transcript so the agent has the context.
334
+ transcript = "\n\n".join(
335
+ f"{m['role'].upper()}: {m['content']}" for m in history
336
+ )
337
+
338
+ # Snapshot existing features so we can compute what was created.
339
+ existing_before = len(fm.list_features(project_id))
340
+
341
+ from core.agent import CognosAgent
342
+ from execution.tools.forge_feature_tools import forge_project_scope
343
+ from core.anthropic_auth import subscription_auth_scope
344
+
345
+ prompt = (
346
+ f"{FEATURE_GEN_SYSTEM_PROMPT}\n\n"
347
+ f"The bootstrapper chat transcript follows. Generate the "
348
+ f"backlog now via tool calls.\n\n{transcript}"
349
+ )
350
+
351
+ try:
352
+ agent = CognosAgent()
353
+ with subscription_auth_scope(), forge_project_scope(project_id):
354
+ resp = await agent.chat(prompt, stream=False)
355
+ except Exception as e:
356
+ logger.exception("generate-features agent failed")
357
+ raise HTTPException(502, f"agent failed: {e}")
358
+
359
+ summary = str(getattr(resp, "content", resp) or "")[:500]
360
+ features_after = fm.list_features(project_id)
361
+ created = len(features_after) - existing_before
362
+ if created <= 0:
363
+ raise HTTPException(
364
+ 502,
365
+ "agent finished without creating any features. Try again "
366
+ "with a clearer description.",
367
+ )
368
+ # Auto-close the bootstrapper session
369
+ _close_session(session_id, "completed")
370
+ return {
371
+ "count": created,
372
+ "total": len(features_after),
373
+ "project_id": project_id,
374
+ "summary": summary,
375
+ }
376
+
377
+ return router