agentic-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.
@@ -0,0 +1,55 @@
1
+ """Agentic CLI - A framework for building domain-specific agentic CLI applications.
2
+
3
+ This package provides the core infrastructure for building CLI applications
4
+ powered by LLM agents, including:
5
+
6
+ - CLI framework with thinking boxes and rich output
7
+ - Workflow management for agent orchestration
8
+ - Generic tools (search, code execution, document generation)
9
+ - Knowledge base with vector search
10
+ - Session persistence
11
+
12
+ Domain-specific applications extend the base classes to provide their own
13
+ agents, prompts, and configuration.
14
+ """
15
+
16
+ from agentic_cli.cli.app import BaseCLIApp
17
+ from agentic_cli.cli.commands import Command, CommandRegistry
18
+ from agentic_cli.workflow.manager import WorkflowManager
19
+ from agentic_cli.workflow.config import AgentConfig
20
+ from agentic_cli.workflow.events import WorkflowEvent, EventType
21
+ from agentic_cli.config import (
22
+ BaseSettings,
23
+ SettingsContext,
24
+ SettingsValidationError,
25
+ get_settings,
26
+ set_settings,
27
+ set_context_settings,
28
+ get_context_settings,
29
+ validate_settings,
30
+ reload_settings,
31
+ )
32
+
33
+ __all__ = [
34
+ # CLI
35
+ "BaseCLIApp",
36
+ "Command",
37
+ "CommandRegistry",
38
+ # Workflow
39
+ "WorkflowManager",
40
+ "AgentConfig",
41
+ "WorkflowEvent",
42
+ "EventType",
43
+ # Settings
44
+ "BaseSettings",
45
+ "SettingsContext",
46
+ "SettingsValidationError",
47
+ "get_settings",
48
+ "set_settings",
49
+ "set_context_settings",
50
+ "get_context_settings",
51
+ "validate_settings",
52
+ "reload_settings",
53
+ ]
54
+
55
+ __version__ = "0.1.0"
@@ -0,0 +1,22 @@
1
+ """CLI framework for agentic applications."""
2
+
3
+ from thinking_prompt import AppInfo
4
+
5
+ from agentic_cli.cli.commands import (
6
+ Command,
7
+ CommandCategory,
8
+ CommandRegistry,
9
+ ParsedArgs,
10
+ create_simple_command,
11
+ )
12
+ from agentic_cli.cli.app import BaseCLIApp
13
+
14
+ __all__ = [
15
+ "AppInfo",
16
+ "BaseCLIApp",
17
+ "Command",
18
+ "CommandCategory",
19
+ "CommandRegistry",
20
+ "ParsedArgs",
21
+ "create_simple_command",
22
+ ]
agentic_cli/cli/app.py ADDED
@@ -0,0 +1,487 @@
1
+ """Base CLI Application for agentic applications.
2
+
3
+ This module provides the base CLI application that:
4
+ 1. Uses ThinkingPromptSession for all UI (thinking boxes, messages, etc.)
5
+ 2. Connects to domain-specific workflow managers
6
+ 3. Tracks message history for persistence
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from abc import ABC, abstractmethod
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from enum import Enum
17
+ from typing import TYPE_CHECKING, Any, Callable
18
+
19
+ from prompt_toolkit.completion import WordCompleter
20
+ from prompt_toolkit.history import InMemoryHistory
21
+ from rich.console import Console
22
+
23
+ from thinking_prompt import ThinkingPromptSession, AppInfo
24
+ from thinking_prompt.styles import ThinkingPromptStyles
25
+
26
+ from agentic_cli.cli.commands import Command, CommandRegistry
27
+ from agentic_cli.config import BaseSettings
28
+ from agentic_cli.logging import Loggers, configure_logging, bind_context
29
+
30
+ if TYPE_CHECKING:
31
+ from agentic_cli.workflow import WorkflowManager, EventType
32
+
33
+ logger = Loggers.cli()
34
+
35
+ # Thread pool for background initialization (single worker)
36
+ _init_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="workflow-init")
37
+
38
+
39
+ # === Message History for Persistence ===
40
+
41
+
42
+ class MessageType(Enum):
43
+ """Types of messages in history."""
44
+
45
+ USER = "user"
46
+ ASSISTANT = "assistant"
47
+ SYSTEM = "system"
48
+ ERROR = "error"
49
+ WARNING = "warning"
50
+ SUCCESS = "success"
51
+ THINKING = "thinking"
52
+
53
+
54
+ @dataclass
55
+ class Message:
56
+ """A message stored in history for persistence."""
57
+
58
+ content: str
59
+ message_type: MessageType
60
+ timestamp: datetime = field(default_factory=datetime.now)
61
+ metadata: dict = field(default_factory=dict)
62
+
63
+
64
+ class MessageHistory:
65
+ """Simple message history for persistence."""
66
+
67
+ def __init__(self) -> None:
68
+ self._messages: list[Message] = []
69
+
70
+ def add(
71
+ self,
72
+ content: str,
73
+ message_type: MessageType | str,
74
+ timestamp: datetime | None = None,
75
+ **metadata: object,
76
+ ) -> None:
77
+ """Add a message to history."""
78
+ if isinstance(message_type, str):
79
+ message_type = MessageType(message_type)
80
+ self._messages.append(
81
+ Message(
82
+ content=content,
83
+ message_type=message_type,
84
+ timestamp=timestamp or datetime.now(),
85
+ metadata=dict(metadata),
86
+ )
87
+ )
88
+
89
+ def get_all(self) -> list[Message]:
90
+ """Get all messages."""
91
+ return list(self._messages)
92
+
93
+ def get_by_type(self, message_type: MessageType) -> list[Message]:
94
+ """Get messages of a specific type."""
95
+ return [m for m in self._messages if m.message_type == message_type]
96
+
97
+ def clear(self) -> None:
98
+ """Clear all messages."""
99
+ self._messages.clear()
100
+
101
+ def __len__(self) -> int:
102
+ return len(self._messages)
103
+
104
+
105
+ # === Default Styles ===
106
+
107
+
108
+ def get_default_styles() -> ThinkingPromptStyles:
109
+ """Get default styles for ThinkingPromptSession."""
110
+ return ThinkingPromptStyles(
111
+ thinking_box="fg:#6c757d",
112
+ thinking_box_border="fg:#495057",
113
+ thinking_box_hint="fg:#6c757d italic",
114
+ user_message="fg:#17a2b8",
115
+ assistant_message="fg:#e9ecef",
116
+ system_message="fg:#6c757d italic",
117
+ error_message="fg:#dc3545 bold",
118
+ warning_message="fg:#ffc107",
119
+ success_message="fg:#28a745",
120
+ )
121
+
122
+
123
+ # === Base CLI Application ===
124
+
125
+
126
+ class BaseCLIApp(ABC):
127
+ """
128
+ Base CLI Application for agentic applications.
129
+
130
+ Domain-specific applications extend this class and implement:
131
+ - get_app_info(): Provide app name, version, welcome message
132
+ - get_settings(): Provide domain-specific settings
133
+ - create_workflow_manager(): Create domain-specific workflow manager
134
+ - register_commands(): Register domain-specific commands (optional)
135
+ """
136
+
137
+ def __init__(self, settings: BaseSettings | None = None) -> None:
138
+ """Initialize the CLI application.
139
+
140
+ Args:
141
+ settings: Optional settings override
142
+ """
143
+ # === Configuration ===
144
+ self._settings = settings or self.get_settings()
145
+ configure_logging(self._settings)
146
+
147
+ logger.info("app_starting", app_name=self._settings.app_name)
148
+
149
+ # === Message History (for persistence) ===
150
+ self.message_history = MessageHistory()
151
+
152
+ # === Command Registry ===
153
+ self.command_registry = CommandRegistry()
154
+ self._register_builtin_commands()
155
+ self.register_commands() # Domain-specific commands
156
+
157
+ # === Workflow Manager (initialized in background) ===
158
+ self._workflow: WorkflowManager | None = None
159
+ self._init_task: asyncio.Task[None] | None = None
160
+ self._init_error: Exception | None = None
161
+
162
+ # === UI: ThinkingPromptSession ===
163
+ command_completions = [
164
+ "/" + name for name in self.command_registry.get_completions()
165
+ ]
166
+ completer = WordCompleter(command_completions, ignore_case=True)
167
+
168
+ self.session = ThinkingPromptSession(
169
+ message=">>> ",
170
+ app_info=self.get_app_info(),
171
+ styles=self.get_styles(),
172
+ history=InMemoryHistory(),
173
+ completer=completer,
174
+ enable_status_bar=True,
175
+ status_text="Ctrl+C: cancel | Ctrl+D: exit | /help: commands",
176
+ )
177
+
178
+ # === State ===
179
+ self.should_exit = False
180
+
181
+ # === Rich Console (for commands that need direct console access) ===
182
+ self.console = Console()
183
+
184
+ logger.debug("app_initialized_fast")
185
+
186
+ @abstractmethod
187
+ def get_app_info(self) -> AppInfo:
188
+ """Get the application info for ThinkingPromptSession.
189
+
190
+ Domain projects implement this to provide their app name,
191
+ version, and welcome message.
192
+ """
193
+ ...
194
+
195
+ @abstractmethod
196
+ def get_settings(self) -> BaseSettings:
197
+ """Get the application settings.
198
+
199
+ Domain projects implement this to provide their settings class.
200
+ """
201
+ ...
202
+
203
+ @abstractmethod
204
+ def create_workflow_manager(self) -> "WorkflowManager":
205
+ """Create the workflow manager for this domain.
206
+
207
+ Domain projects implement this to create their workflow manager
208
+ with domain-specific agents.
209
+ """
210
+ ...
211
+
212
+ def get_styles(self) -> ThinkingPromptStyles:
213
+ """Get styles for ThinkingPromptSession.
214
+
215
+ Override to customize styles.
216
+ """
217
+ return get_default_styles()
218
+
219
+ def register_commands(self) -> None:
220
+ """Register domain-specific commands.
221
+
222
+ Override to register additional commands.
223
+ """
224
+ pass
225
+
226
+ @property
227
+ def workflow(self) -> "WorkflowManager":
228
+ """Get the workflow manager.
229
+
230
+ Raises:
231
+ RuntimeError: If workflow is not yet initialized
232
+ """
233
+ if self._workflow is None:
234
+ raise RuntimeError("Workflow not initialized yet")
235
+ return self._workflow
236
+
237
+ @property
238
+ def default_user(self) -> str:
239
+ """Get the default user name."""
240
+ return self._settings.default_user
241
+
242
+ @property
243
+ def settings(self) -> BaseSettings:
244
+ """Get the application settings."""
245
+ return self._settings
246
+
247
+ def stop(self) -> None:
248
+ """Stop the application."""
249
+ self.should_exit = True
250
+ self.session.exit()
251
+
252
+ def _register_builtin_commands(self) -> None:
253
+ """Register built-in commands."""
254
+ from agentic_cli.cli.builtin_commands import (
255
+ HelpCommand,
256
+ ClearCommand,
257
+ ExitCommand,
258
+ StatusCommand,
259
+ SaveCommand,
260
+ LoadCommand,
261
+ SessionsCommand,
262
+ SettingsCommand,
263
+ )
264
+
265
+ self.command_registry.register(HelpCommand())
266
+ self.command_registry.register(ClearCommand())
267
+ self.command_registry.register(ExitCommand())
268
+ self.command_registry.register(StatusCommand())
269
+ self.command_registry.register(SaveCommand())
270
+ self.command_registry.register(LoadCommand())
271
+ self.command_registry.register(SessionsCommand())
272
+ self.command_registry.register(SettingsCommand())
273
+
274
+ async def _background_init(self) -> None:
275
+ """Initialize WorkflowManager in background thread."""
276
+ loop = asyncio.get_running_loop()
277
+
278
+ def _create_workflow() -> "WorkflowManager":
279
+ return self.create_workflow_manager()
280
+
281
+ try:
282
+ logger.debug("background_init_starting")
283
+ self._workflow = await loop.run_in_executor(
284
+ _init_executor, _create_workflow
285
+ )
286
+
287
+ # Update status bar to show ready
288
+ model = self._workflow.model
289
+ self.session.status_text = f"{model} | Ctrl+C: cancel | /help: commands"
290
+ logger.info("background_init_complete", model=model)
291
+
292
+ except Exception as e:
293
+ self._init_error = e
294
+ # Don't log to console - error will be shown via UI when user interacts
295
+ self.session.status_text = "Init failed - check API keys"
296
+
297
+ async def _ensure_initialized(self) -> bool:
298
+ """Wait for background initialization to complete.
299
+
300
+ Returns:
301
+ True if initialization succeeded, False otherwise
302
+ """
303
+ if self._workflow is not None:
304
+ return True
305
+
306
+ if self._init_task is None:
307
+ return False
308
+
309
+ if not self._init_task.done():
310
+ # Show user we're waiting for initialization
311
+ self.session.start_thinking(lambda: "Waiting for initialization...")
312
+ try:
313
+ await self._init_task
314
+ finally:
315
+ self.session.finish_thinking(add_to_history=False)
316
+
317
+ if self._init_error:
318
+ self.session.add_error(f"Initialization failed: {self._init_error}")
319
+ return False
320
+
321
+ return self._workflow is not None
322
+
323
+ async def process_input(self, user_input: str) -> None:
324
+ """Process user input.
325
+
326
+ Args:
327
+ user_input: The raw user input string
328
+ """
329
+ user_input = user_input.strip()
330
+
331
+ if not user_input:
332
+ return
333
+
334
+ # Route to appropriate handler
335
+ if user_input.startswith("/"):
336
+ await self._handle_command(user_input)
337
+ else:
338
+ await self._handle_message(user_input)
339
+
340
+ async def _handle_command(self, user_input: str) -> None:
341
+ """Handle slash command execution.
342
+
343
+ Args:
344
+ user_input: The command string starting with /
345
+ """
346
+ parts = user_input[1:].split(maxsplit=1)
347
+ command_name = parts[0] if parts else ""
348
+ args = parts[1] if len(parts) > 1 else ""
349
+
350
+ command = self.command_registry.get(command_name)
351
+ if command:
352
+ logger.debug("executing_command", command=command.name, args=args)
353
+ try:
354
+ self.session.add_response(f"Executing command: /{command.name} {args}")
355
+ await command.execute(args, self)
356
+ logger.debug("command_completed", command=command.name)
357
+ except Exception as e:
358
+ self.session.add_error(f"Error executing command: {e}")
359
+ else:
360
+ self.session.add_error(f"Unknown command: /{command_name}")
361
+ self.session.add_message("system", "Type /help to see available commands")
362
+
363
+ async def _handle_message(self, message: str) -> None:
364
+ """Route message through agentic workflow.
365
+
366
+ Args:
367
+ message: User message to process
368
+ """
369
+ # Wait for initialization if needed
370
+ if not await self._ensure_initialized():
371
+ self.session.add_error(
372
+ "Cannot process message - workflow not initialized. "
373
+ "Please check your API keys (GOOGLE_API_KEY or ANTHROPIC_API_KEY)."
374
+ )
375
+ return
376
+
377
+ # Import EventType here (workflow module is now loaded)
378
+ from agentic_cli.workflow import EventType
379
+
380
+ bind_context(user_id=self._settings.default_user)
381
+ logger.info("handling_message", message_length=len(message))
382
+
383
+ # Track message in history
384
+ self.message_history.add(message, MessageType.USER)
385
+
386
+ # Status line for thinking box (single line updates)
387
+ status_line = "Processing..."
388
+ thinking_started = False
389
+
390
+ # Accumulate content for history
391
+ thinking_content: list[str] = []
392
+ response_content: list[str] = []
393
+
394
+ def get_status() -> str:
395
+ return status_line
396
+
397
+ try:
398
+ self.session.start_thinking(get_status)
399
+ thinking_started = True
400
+
401
+ async for event in self.workflow.process(
402
+ message=message,
403
+ user_id=self._settings.default_user,
404
+ ):
405
+ if event.type == EventType.TEXT:
406
+ # Stream response directly to console
407
+ self.session.add_response(event.content, markdown=True)
408
+ response_content.append(event.content)
409
+
410
+ elif event.type == EventType.THINKING:
411
+ # Stream thinking directly to console
412
+ status_line = "Thinking..."
413
+ self.session.add_message("system", event.content)
414
+ thinking_content.append(event.content)
415
+
416
+ elif event.type == EventType.TOOL_CALL:
417
+ # Update status line in thinking box
418
+ tool_name = event.metadata.get("tool_name", "unknown")
419
+ status_line = f"Calling: {tool_name}"
420
+
421
+ elif event.type == EventType.CODE_EXECUTION:
422
+ # Update status with execution result
423
+ result_preview = (
424
+ event.content[:40] + "..."
425
+ if len(event.content) > 40
426
+ else event.content
427
+ )
428
+ status_line = f"Result: {result_preview}"
429
+
430
+ elif event.type == EventType.EXECUTABLE_CODE:
431
+ # Update status when executing code
432
+ lang = event.metadata.get("language", "python")
433
+ status_line = f"Running {lang} code..."
434
+
435
+ elif event.type == EventType.FILE_DATA:
436
+ # Update status with file info
437
+ status_line = f"File: {event.content}"
438
+
439
+ # Finish thinking box (don't add status to history)
440
+ if thinking_started:
441
+ self.session.finish_thinking(add_to_history=False)
442
+
443
+ # Add accumulated content to message history
444
+ if thinking_content:
445
+ self.message_history.add(
446
+ "".join(thinking_content), MessageType.THINKING
447
+ )
448
+ if response_content:
449
+ self.message_history.add(
450
+ "".join(response_content), MessageType.ASSISTANT
451
+ )
452
+
453
+ logger.debug("message_handled_successfully")
454
+
455
+ except Exception as e:
456
+ if thinking_started:
457
+ self.session.finish_thinking(add_to_history=False)
458
+ self.session.add_error(f"Workflow error: {e}")
459
+ self.message_history.add(str(e), MessageType.ERROR)
460
+
461
+ async def run(self) -> None:
462
+ """Run the main application loop."""
463
+ logger.info("repl_starting")
464
+
465
+ # Start background initialization (non-blocking)
466
+ self._init_task = asyncio.create_task(self._background_init())
467
+
468
+ # Register input handler
469
+ @self.session.on_input
470
+ async def handle_input(text: str) -> None:
471
+ if self.should_exit:
472
+ return
473
+ await self.process_input(text)
474
+
475
+ # Run the session - user sees prompt immediately!
476
+ await self.session.run_async()
477
+
478
+ # Cleanup: cancel init task if still running
479
+ if self._init_task and not self._init_task.done():
480
+ self._init_task.cancel()
481
+ try:
482
+ await self._init_task
483
+ except asyncio.CancelledError:
484
+ pass
485
+
486
+ logger.info("app_ending")
487
+ self.session.add_message("system", "Goodbye!")