code-puppy 0.0.354__py3-none-any.whl → 0.0.356__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.
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/event_stream_handler.py +74 -1
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +92 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +198 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +191 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/config.py +63 -0
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/rich_renderer.py +34 -0
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/agent_tools.py +53 -49
- code_puppy/tools/command_runner.py +292 -100
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +6 -1
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/METADATA +4 -3
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/RECORD +38 -21
- {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/licenses/LICENSE +0 -0
code_puppy/agents/__init__.py
CHANGED
|
@@ -12,6 +12,7 @@ from .agent_manager import (
|
|
|
12
12
|
refresh_agents,
|
|
13
13
|
set_current_agent,
|
|
14
14
|
)
|
|
15
|
+
from .subagent_stream_handler import subagent_stream_handler
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"get_available_agents",
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"load_agent",
|
|
21
22
|
"get_agent_descriptions",
|
|
22
23
|
"refresh_agents",
|
|
24
|
+
"subagent_stream_handler",
|
|
23
25
|
]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Event stream handler for processing streaming events from agent runs."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
3
5
|
from collections.abc import AsyncIterable
|
|
4
6
|
from typing import Any, Optional
|
|
5
7
|
|
|
@@ -16,8 +18,35 @@ from rich.console import Console
|
|
|
16
18
|
from rich.markup import escape
|
|
17
19
|
from rich.text import Text
|
|
18
20
|
|
|
19
|
-
from code_puppy.config import get_banner_color
|
|
21
|
+
from code_puppy.config import get_banner_color, get_subagent_verbose
|
|
20
22
|
from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
|
|
23
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fire_stream_event(event_type: str, event_data: Any) -> None:
|
|
29
|
+
"""Fire a stream event callback asynchronously (non-blocking).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
event_type: Type of the event (e.g., 'part_start', 'part_delta', 'part_end')
|
|
33
|
+
event_data: Data associated with the event
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
from code_puppy import callbacks
|
|
37
|
+
from code_puppy.messaging import get_session_context
|
|
38
|
+
|
|
39
|
+
agent_session_id = get_session_context()
|
|
40
|
+
|
|
41
|
+
# Use create_task to fire callback without blocking
|
|
42
|
+
asyncio.create_task(
|
|
43
|
+
callbacks.on_stream_event(event_type, event_data, agent_session_id)
|
|
44
|
+
)
|
|
45
|
+
except ImportError:
|
|
46
|
+
logger.debug("callbacks or messaging module not available for stream event")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.debug(f"Error firing stream event callback: {e}")
|
|
49
|
+
|
|
21
50
|
|
|
22
51
|
# Module-level console for streaming output
|
|
23
52
|
# Set via set_streaming_console() to share console with spinner
|
|
@@ -47,6 +76,15 @@ def get_streaming_console() -> Console:
|
|
|
47
76
|
return Console()
|
|
48
77
|
|
|
49
78
|
|
|
79
|
+
def _should_suppress_output() -> bool:
|
|
80
|
+
"""Check if sub-agent output should be suppressed.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if we're in a sub-agent context and verbose mode is disabled.
|
|
84
|
+
"""
|
|
85
|
+
return is_subagent() and not get_subagent_verbose()
|
|
86
|
+
|
|
87
|
+
|
|
50
88
|
async def event_stream_handler(
|
|
51
89
|
ctx: RunContext,
|
|
52
90
|
events: AsyncIterable[Any],
|
|
@@ -60,6 +98,12 @@ async def event_stream_handler(
|
|
|
60
98
|
ctx: The run context.
|
|
61
99
|
events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
|
|
62
100
|
"""
|
|
101
|
+
# If we're in a sub-agent and verbose mode is disabled, silently consume events
|
|
102
|
+
if _should_suppress_output():
|
|
103
|
+
async for _ in events:
|
|
104
|
+
pass # Just consume events without rendering
|
|
105
|
+
return
|
|
106
|
+
|
|
63
107
|
import time
|
|
64
108
|
|
|
65
109
|
from termflow import Parser as TermflowParser
|
|
@@ -121,6 +165,16 @@ async def event_stream_handler(
|
|
|
121
165
|
async for event in events:
|
|
122
166
|
# PartStartEvent - register the part but defer banner until content arrives
|
|
123
167
|
if isinstance(event, PartStartEvent):
|
|
168
|
+
# Fire stream event callback for part_start
|
|
169
|
+
_fire_stream_event(
|
|
170
|
+
"part_start",
|
|
171
|
+
{
|
|
172
|
+
"index": event.index,
|
|
173
|
+
"part_type": type(event.part).__name__,
|
|
174
|
+
"part": event.part,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
124
178
|
part = event.part
|
|
125
179
|
if isinstance(part, ThinkingPart):
|
|
126
180
|
streaming_parts.add(event.index)
|
|
@@ -156,6 +210,16 @@ async def event_stream_handler(
|
|
|
156
210
|
|
|
157
211
|
# PartDeltaEvent - stream the content as it arrives
|
|
158
212
|
elif isinstance(event, PartDeltaEvent):
|
|
213
|
+
# Fire stream event callback for part_delta
|
|
214
|
+
_fire_stream_event(
|
|
215
|
+
"part_delta",
|
|
216
|
+
{
|
|
217
|
+
"index": event.index,
|
|
218
|
+
"delta_type": type(event.delta).__name__,
|
|
219
|
+
"delta": event.delta,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
159
223
|
if event.index in streaming_parts:
|
|
160
224
|
delta = event.delta
|
|
161
225
|
if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
|
|
@@ -208,6 +272,15 @@ async def event_stream_handler(
|
|
|
208
272
|
|
|
209
273
|
# PartEndEvent - finish the streaming with a newline
|
|
210
274
|
elif isinstance(event, PartEndEvent):
|
|
275
|
+
# Fire stream event callback for part_end
|
|
276
|
+
_fire_stream_event(
|
|
277
|
+
"part_end",
|
|
278
|
+
{
|
|
279
|
+
"index": event.index,
|
|
280
|
+
"next_part_kind": getattr(event, "next_part_kind", None),
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
211
284
|
if event.index in streaming_parts:
|
|
212
285
|
# For text parts, finalize termflow rendering
|
|
213
286
|
if event.index in text_parts:
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Silenced event stream handler for sub-agents.
|
|
2
|
+
|
|
3
|
+
This handler suppresses all console output but still:
|
|
4
|
+
- Updates SubAgentConsoleManager with status/metrics
|
|
5
|
+
- Fires stream_event callbacks for the frontend emitter plugin
|
|
6
|
+
- Tracks tool calls, tokens, and status changes
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
>>> from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
|
|
10
|
+
>>> # In agent run:
|
|
11
|
+
>>> await subagent_stream_handler(ctx, events, session_id="my-session-123")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
from collections.abc import AsyncIterable
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from pydantic_ai import PartDeltaEvent, PartEndEvent, PartStartEvent, RunContext
|
|
20
|
+
from pydantic_ai.messages import (
|
|
21
|
+
TextPart,
|
|
22
|
+
TextPartDelta,
|
|
23
|
+
ThinkingPart,
|
|
24
|
+
ThinkingPartDelta,
|
|
25
|
+
ToolCallPart,
|
|
26
|
+
ToolCallPartDelta,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# Callback Helper
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fire_callback(event_type: str, event_data: Any, session_id: Optional[str]) -> None:
|
|
38
|
+
"""Fire stream_event callback non-blocking.
|
|
39
|
+
|
|
40
|
+
Schedules the callback to run asynchronously without waiting for it.
|
|
41
|
+
Silently ignores errors if no event loop is running or if the callback
|
|
42
|
+
system is unavailable.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
event_type: Type of the event ('part_start', 'part_delta', 'part_end')
|
|
46
|
+
event_data: Dictionary containing event-specific data
|
|
47
|
+
session_id: Optional session ID for the sub-agent
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
from code_puppy import callbacks
|
|
51
|
+
|
|
52
|
+
loop = asyncio.get_running_loop()
|
|
53
|
+
loop.create_task(callbacks.on_stream_event(event_type, event_data, session_id))
|
|
54
|
+
except RuntimeError:
|
|
55
|
+
# No event loop running - this can happen during shutdown
|
|
56
|
+
logger.debug("No event loop available for stream event callback")
|
|
57
|
+
except ImportError:
|
|
58
|
+
# Callbacks module not available
|
|
59
|
+
logger.debug("Callbacks module not available for stream event")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
# Don't let callback errors break the stream handler
|
|
62
|
+
logger.debug(f"Error firing stream event callback: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# Token Estimation
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _estimate_tokens(content: str) -> int:
|
|
71
|
+
"""Estimate token count from content string.
|
|
72
|
+
|
|
73
|
+
Uses a rough heuristic: ~4 characters per token for English text.
|
|
74
|
+
This is a ballpark estimate - actual tokenization varies by model.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
content: The text content to estimate tokens for
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Estimated token count (minimum 1 for non-empty content)
|
|
81
|
+
"""
|
|
82
|
+
if not content:
|
|
83
|
+
return 0
|
|
84
|
+
# Rough estimate: 4 chars = 1 token, minimum 1 for any content
|
|
85
|
+
return max(1, len(content) // 4)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Main Handler
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def subagent_stream_handler(
|
|
94
|
+
ctx: RunContext,
|
|
95
|
+
events: AsyncIterable[Any],
|
|
96
|
+
session_id: Optional[str] = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Silent event stream handler for sub-agents.
|
|
99
|
+
|
|
100
|
+
Processes streaming events without producing any console output.
|
|
101
|
+
Updates the SubAgentConsoleManager with status and metrics, and fires
|
|
102
|
+
stream_event callbacks for any registered listeners.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
ctx: The pydantic-ai run context
|
|
106
|
+
events: Async iterable of streaming events (PartStartEvent,
|
|
107
|
+
PartDeltaEvent, PartEndEvent)
|
|
108
|
+
session_id: Session ID of the sub-agent for console manager updates.
|
|
109
|
+
If None, falls back to get_session_context().
|
|
110
|
+
"""
|
|
111
|
+
# Late import to avoid circular dependencies
|
|
112
|
+
from code_puppy.messaging import get_session_context
|
|
113
|
+
from code_puppy.messaging.subagent_console import SubAgentConsoleManager
|
|
114
|
+
|
|
115
|
+
manager = SubAgentConsoleManager.get_instance()
|
|
116
|
+
|
|
117
|
+
# Resolve session_id, falling back to context if not provided
|
|
118
|
+
effective_session_id = session_id or get_session_context()
|
|
119
|
+
|
|
120
|
+
# Metrics tracking
|
|
121
|
+
token_count = 0
|
|
122
|
+
tool_call_count = 0
|
|
123
|
+
active_tool_parts: set[int] = set() # Track active tool call indices
|
|
124
|
+
|
|
125
|
+
async for event in events:
|
|
126
|
+
try:
|
|
127
|
+
await _handle_event(
|
|
128
|
+
event=event,
|
|
129
|
+
manager=manager,
|
|
130
|
+
session_id=effective_session_id,
|
|
131
|
+
token_count=token_count,
|
|
132
|
+
tool_call_count=tool_call_count,
|
|
133
|
+
active_tool_parts=active_tool_parts,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Update metrics from returned values
|
|
137
|
+
# (we need to track these at this level since they're modified in _handle_event)
|
|
138
|
+
if isinstance(event, PartStartEvent):
|
|
139
|
+
if isinstance(event.part, ToolCallPart):
|
|
140
|
+
tool_call_count += 1
|
|
141
|
+
active_tool_parts.add(event.index)
|
|
142
|
+
|
|
143
|
+
elif isinstance(event, PartDeltaEvent):
|
|
144
|
+
delta = event.delta
|
|
145
|
+
if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
|
|
146
|
+
if delta.content_delta:
|
|
147
|
+
token_count += _estimate_tokens(delta.content_delta)
|
|
148
|
+
|
|
149
|
+
elif isinstance(event, PartEndEvent):
|
|
150
|
+
active_tool_parts.discard(event.index)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
# Log but don't crash on event handling errors
|
|
154
|
+
logger.debug(f"Error handling stream event: {e}")
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _handle_event(
|
|
159
|
+
event: Any,
|
|
160
|
+
manager: Any, # SubAgentConsoleManager
|
|
161
|
+
session_id: Optional[str],
|
|
162
|
+
token_count: int,
|
|
163
|
+
tool_call_count: int,
|
|
164
|
+
active_tool_parts: set[int],
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Handle a single streaming event.
|
|
167
|
+
|
|
168
|
+
Updates the console manager and fires callbacks for each event type.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
event: The streaming event to handle
|
|
172
|
+
manager: SubAgentConsoleManager instance
|
|
173
|
+
session_id: Session ID for updates
|
|
174
|
+
token_count: Current token count
|
|
175
|
+
tool_call_count: Current tool call count
|
|
176
|
+
active_tool_parts: Set of active tool call indices
|
|
177
|
+
"""
|
|
178
|
+
if session_id is None:
|
|
179
|
+
# Can't update manager without session_id
|
|
180
|
+
logger.debug("No session_id available for stream event")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# -------------------------------------------------------------------------
|
|
184
|
+
# PartStartEvent - Track new parts and update status
|
|
185
|
+
# -------------------------------------------------------------------------
|
|
186
|
+
if isinstance(event, PartStartEvent):
|
|
187
|
+
part = event.part
|
|
188
|
+
event_data = {
|
|
189
|
+
"index": event.index,
|
|
190
|
+
"part_type": type(part).__name__,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if isinstance(part, ThinkingPart):
|
|
194
|
+
manager.update_agent(session_id, status="thinking")
|
|
195
|
+
event_data["content"] = getattr(part, "content", None)
|
|
196
|
+
|
|
197
|
+
elif isinstance(part, TextPart):
|
|
198
|
+
manager.update_agent(session_id, status="running")
|
|
199
|
+
event_data["content"] = getattr(part, "content", None)
|
|
200
|
+
|
|
201
|
+
elif isinstance(part, ToolCallPart):
|
|
202
|
+
# tool_call_count is updated in the main handler
|
|
203
|
+
manager.update_agent(
|
|
204
|
+
session_id,
|
|
205
|
+
status="tool_calling",
|
|
206
|
+
tool_call_count=tool_call_count + 1, # +1 for this new one
|
|
207
|
+
current_tool=part.tool_name,
|
|
208
|
+
)
|
|
209
|
+
event_data["tool_name"] = part.tool_name
|
|
210
|
+
event_data["tool_call_id"] = getattr(part, "tool_call_id", None)
|
|
211
|
+
|
|
212
|
+
_fire_callback("part_start", event_data, session_id)
|
|
213
|
+
|
|
214
|
+
# -------------------------------------------------------------------------
|
|
215
|
+
# PartDeltaEvent - Track content deltas and update metrics
|
|
216
|
+
# -------------------------------------------------------------------------
|
|
217
|
+
elif isinstance(event, PartDeltaEvent):
|
|
218
|
+
delta = event.delta
|
|
219
|
+
event_data = {
|
|
220
|
+
"index": event.index,
|
|
221
|
+
"delta_type": type(delta).__name__,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if isinstance(delta, TextPartDelta):
|
|
225
|
+
content_delta = delta.content_delta
|
|
226
|
+
if content_delta:
|
|
227
|
+
# Token count is updated in main handler
|
|
228
|
+
new_token_count = token_count + _estimate_tokens(content_delta)
|
|
229
|
+
manager.update_agent(session_id, token_count=new_token_count)
|
|
230
|
+
event_data["content_delta"] = content_delta
|
|
231
|
+
|
|
232
|
+
elif isinstance(delta, ThinkingPartDelta):
|
|
233
|
+
content_delta = delta.content_delta
|
|
234
|
+
if content_delta:
|
|
235
|
+
new_token_count = token_count + _estimate_tokens(content_delta)
|
|
236
|
+
manager.update_agent(session_id, token_count=new_token_count)
|
|
237
|
+
event_data["content_delta"] = content_delta
|
|
238
|
+
|
|
239
|
+
elif isinstance(delta, ToolCallPartDelta):
|
|
240
|
+
# Tool call deltas might have partial args
|
|
241
|
+
event_data["args_delta"] = getattr(delta, "args_delta", None)
|
|
242
|
+
event_data["tool_name_delta"] = getattr(delta, "tool_name_delta", None)
|
|
243
|
+
|
|
244
|
+
_fire_callback("part_delta", event_data, session_id)
|
|
245
|
+
|
|
246
|
+
# -------------------------------------------------------------------------
|
|
247
|
+
# PartEndEvent - Track part completion and update status
|
|
248
|
+
# -------------------------------------------------------------------------
|
|
249
|
+
elif isinstance(event, PartEndEvent):
|
|
250
|
+
event_data = {
|
|
251
|
+
"index": event.index,
|
|
252
|
+
"next_part_kind": getattr(event, "next_part_kind", None),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# If this was a tool call part ending, check if we should reset status
|
|
256
|
+
if event.index in active_tool_parts:
|
|
257
|
+
# Remove this index from active parts (done in main handler)
|
|
258
|
+
# If no more active tool parts after removal, reset to running
|
|
259
|
+
remaining_active = active_tool_parts - {event.index}
|
|
260
|
+
if not remaining_active:
|
|
261
|
+
manager.update_agent(
|
|
262
|
+
session_id,
|
|
263
|
+
current_tool=None,
|
|
264
|
+
status="running",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
_fire_callback("part_end", event_data, session_id)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =============================================================================
|
|
271
|
+
# Exports
|
|
272
|
+
# =============================================================================
|
|
273
|
+
|
|
274
|
+
__all__ = [
|
|
275
|
+
"subagent_stream_handler",
|
|
276
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Code Puppy REST API module.
|
|
2
|
+
|
|
3
|
+
This module provides a FastAPI-based REST API for Code Puppy configuration,
|
|
4
|
+
sessions, commands, and real-time WebSocket communication.
|
|
5
|
+
|
|
6
|
+
Exports:
|
|
7
|
+
create_app: Factory function to create the FastAPI application
|
|
8
|
+
main: Entry point to run the server
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from code_puppy.api.app import create_app
|
|
12
|
+
|
|
13
|
+
__all__ = ["create_app"]
|
code_puppy/api/app.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""FastAPI application factory for Code Puppy API."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from fastapi.responses import FileResponse, HTMLResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_app() -> FastAPI:
|
|
11
|
+
"""Create and configure the FastAPI application."""
|
|
12
|
+
app = FastAPI(
|
|
13
|
+
title="Code Puppy API",
|
|
14
|
+
description="REST API and Interactive Terminal for Code Puppy",
|
|
15
|
+
version="1.0.0",
|
|
16
|
+
docs_url="/docs",
|
|
17
|
+
redoc_url="/redoc",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# CORS middleware for frontend access
|
|
21
|
+
app.add_middleware(
|
|
22
|
+
CORSMiddleware,
|
|
23
|
+
allow_origins=["*"], # Local/trusted
|
|
24
|
+
allow_credentials=True,
|
|
25
|
+
allow_methods=["*"],
|
|
26
|
+
allow_headers=["*"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Include routers
|
|
30
|
+
from code_puppy.api.routers import agents, commands, config, sessions
|
|
31
|
+
|
|
32
|
+
app.include_router(config.router, prefix="/api/config", tags=["config"])
|
|
33
|
+
app.include_router(commands.router, prefix="/api/commands", tags=["commands"])
|
|
34
|
+
app.include_router(sessions.router, prefix="/api/sessions", tags=["sessions"])
|
|
35
|
+
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
|
|
36
|
+
|
|
37
|
+
# WebSocket endpoints (events + terminal)
|
|
38
|
+
from code_puppy.api.websocket import setup_websocket
|
|
39
|
+
|
|
40
|
+
setup_websocket(app)
|
|
41
|
+
|
|
42
|
+
# Templates directory
|
|
43
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
44
|
+
|
|
45
|
+
@app.get("/")
|
|
46
|
+
async def root():
|
|
47
|
+
"""Landing page with links to terminal and docs."""
|
|
48
|
+
return HTMLResponse(
|
|
49
|
+
content="""
|
|
50
|
+
<!DOCTYPE html>
|
|
51
|
+
<html>
|
|
52
|
+
<head>
|
|
53
|
+
<title>Code Puppy 🐶</title>
|
|
54
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
55
|
+
</head>
|
|
56
|
+
<body class="bg-gray-900 text-white min-h-screen flex items-center justify-center">
|
|
57
|
+
<div class="text-center">
|
|
58
|
+
<h1 class="text-6xl mb-4">🐶</h1>
|
|
59
|
+
<h2 class="text-3xl font-bold mb-8">Code Puppy</h2>
|
|
60
|
+
<div class="space-x-4">
|
|
61
|
+
<a href="/terminal" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-lg font-semibold">
|
|
62
|
+
Open Terminal
|
|
63
|
+
</a>
|
|
64
|
+
<a href="/docs" class="px-6 py-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg">
|
|
65
|
+
API Docs
|
|
66
|
+
</a>
|
|
67
|
+
</div>
|
|
68
|
+
<p class="mt-8 text-gray-400">
|
|
69
|
+
WebSocket: ws://localhost:8765/ws/terminal
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@app.get("/terminal")
|
|
78
|
+
async def terminal_page():
|
|
79
|
+
"""Serve the interactive terminal page."""
|
|
80
|
+
html_file = templates_dir / "terminal.html"
|
|
81
|
+
if html_file.exists():
|
|
82
|
+
return FileResponse(html_file, media_type="text/html")
|
|
83
|
+
return HTMLResponse(
|
|
84
|
+
content="<h1>Terminal template not found</h1>",
|
|
85
|
+
status_code=404,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@app.get("/health")
|
|
89
|
+
async def health():
|
|
90
|
+
return {"status": "healthy"}
|
|
91
|
+
|
|
92
|
+
return app
|
code_puppy/api/main.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Entry point for running the FastAPI server."""
|
|
2
|
+
|
|
3
|
+
import uvicorn
|
|
4
|
+
|
|
5
|
+
from code_puppy.api.app import create_app
|
|
6
|
+
|
|
7
|
+
app = create_app()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
11
|
+
"""Run the FastAPI server.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
host: The host address to bind to. Defaults to localhost.
|
|
15
|
+
port: The port number to listen on. Defaults to 8765.
|
|
16
|
+
"""
|
|
17
|
+
uvicorn.run(app, host=host, port=port)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|