kader 0.1.6__tar.gz → 1.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 (98) hide show
  1. kader-0.1.6/README.md → kader-1.1.0/PKG-INFO +55 -0
  2. kader-0.1.6/PKG-INFO → kader-1.1.0/README.md +36 -17
  3. {kader-0.1.6 → kader-1.1.0}/cli/app.py +108 -36
  4. {kader-0.1.6 → kader-1.1.0}/cli/app.tcss +20 -0
  5. kader-1.1.0/cli/llm_factory.py +165 -0
  6. {kader-0.1.6 → kader-1.1.0}/cli/utils.py +19 -11
  7. kader-1.1.0/cli/widgets/conversation.py +101 -0
  8. kader-1.1.0/examples/google_example.py +331 -0
  9. kader-1.1.0/examples/planner_executor_example.py +74 -0
  10. {kader-0.1.6 → kader-1.1.0}/examples/tools_example.py +63 -0
  11. {kader-0.1.6 → kader-1.1.0}/kader/__init__.py +2 -0
  12. {kader-0.1.6 → kader-1.1.0}/kader/agent/agents.py +8 -0
  13. {kader-0.1.6 → kader-1.1.0}/kader/agent/base.py +84 -7
  14. {kader-0.1.6 → kader-1.1.0}/kader/config.py +10 -2
  15. {kader-0.1.6 → kader-1.1.0}/kader/memory/types.py +60 -0
  16. kader-1.1.0/kader/prompts/__init__.py +17 -0
  17. kader-1.1.0/kader/prompts/agent_prompts.py +55 -0
  18. kader-1.1.0/kader/prompts/templates/executor_agent.j2 +70 -0
  19. kader-1.1.0/kader/prompts/templates/kader_planner.j2 +71 -0
  20. {kader-0.1.6 → kader-1.1.0}/kader/providers/__init__.py +2 -0
  21. kader-1.1.0/kader/providers/google.py +690 -0
  22. {kader-0.1.6 → kader-1.1.0}/kader/providers/ollama.py +2 -2
  23. {kader-0.1.6 → kader-1.1.0}/kader/tools/__init__.py +26 -0
  24. kader-1.1.0/kader/tools/agent.py +452 -0
  25. {kader-0.1.6 → kader-1.1.0}/kader/tools/filesys.py +1 -1
  26. {kader-0.1.6 → kader-1.1.0}/kader/tools/todo.py +43 -2
  27. kader-1.1.0/kader/utils/__init__.py +10 -0
  28. kader-1.1.0/kader/utils/checkpointer.py +371 -0
  29. kader-1.1.0/kader/utils/context_aggregator.py +347 -0
  30. kader-1.1.0/kader/workflows/__init__.py +13 -0
  31. kader-1.1.0/kader/workflows/base.py +71 -0
  32. kader-1.1.0/kader/workflows/planner_executor.py +251 -0
  33. {kader-0.1.6 → kader-1.1.0}/pyproject.toml +3 -1
  34. kader-1.1.0/tests/conftest.py +50 -0
  35. kader-1.1.0/tests/providers/test_google.py +505 -0
  36. {kader-0.1.6 → kader-1.1.0}/tests/test_agent_logger_integration.py +21 -11
  37. {kader-0.1.6 → kader-1.1.0}/tests/test_todo_tool.py +58 -5
  38. kader-1.1.0/tests/tools/test_agent_tool.py +275 -0
  39. kader-1.1.0/tests/tools/test_agent_tool_persistence.py +213 -0
  40. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_filesystem_tools.py +3 -3
  41. {kader-0.1.6 → kader-1.1.0}/uv.lock +285 -1
  42. kader-0.1.6/cli/widgets/conversation.py +0 -55
  43. kader-0.1.6/kader/prompts/__init__.py +0 -9
  44. kader-0.1.6/kader/prompts/agent_prompts.py +0 -27
  45. kader-0.1.6/tests/conftest.py +0 -14
  46. {kader-0.1.6 → kader-1.1.0}/.github/workflows/ci.yml +0 -0
  47. {kader-0.1.6 → kader-1.1.0}/.github/workflows/release.yml +0 -0
  48. {kader-0.1.6 → kader-1.1.0}/.gitignore +0 -0
  49. {kader-0.1.6 → kader-1.1.0}/.python-version +0 -0
  50. {kader-0.1.6 → kader-1.1.0}/.qwen/QWEN.md +0 -0
  51. {kader-0.1.6 → kader-1.1.0}/.qwen/agents/technical-writer.md +0 -0
  52. {kader-0.1.6 → kader-1.1.0}/.qwen/agents/test-automation-specialist.md +0 -0
  53. {kader-0.1.6 → kader-1.1.0}/cli/README.md +0 -0
  54. {kader-0.1.6 → kader-1.1.0}/cli/__init__.py +0 -0
  55. {kader-0.1.6 → kader-1.1.0}/cli/__main__.py +0 -0
  56. {kader-0.1.6 → kader-1.1.0}/cli/widgets/__init__.py +0 -0
  57. {kader-0.1.6 → kader-1.1.0}/cli/widgets/confirmation.py +0 -0
  58. {kader-0.1.6 → kader-1.1.0}/cli/widgets/loading.py +0 -0
  59. {kader-0.1.6 → kader-1.1.0}/examples/.gitignore +0 -0
  60. {kader-0.1.6 → kader-1.1.0}/examples/README.md +0 -0
  61. {kader-0.1.6 → kader-1.1.0}/examples/memory_example.py +0 -0
  62. {kader-0.1.6 → kader-1.1.0}/examples/ollama_example.py +0 -0
  63. {kader-0.1.6 → kader-1.1.0}/examples/planning_agent_example.py +0 -0
  64. {kader-0.1.6 → kader-1.1.0}/examples/python_developer/main.py +0 -0
  65. {kader-0.1.6 → kader-1.1.0}/examples/python_developer/template.yaml +0 -0
  66. {kader-0.1.6 → kader-1.1.0}/examples/react_agent_example.py +0 -0
  67. {kader-0.1.6 → kader-1.1.0}/examples/simple_agent.py +0 -0
  68. {kader-0.1.6 → kader-1.1.0}/examples/todo_agent/main.py +0 -0
  69. {kader-0.1.6 → kader-1.1.0}/kader/agent/__init__.py +0 -0
  70. {kader-0.1.6 → kader-1.1.0}/kader/agent/logger.py +0 -0
  71. {kader-0.1.6 → kader-1.1.0}/kader/memory/__init__.py +0 -0
  72. {kader-0.1.6 → kader-1.1.0}/kader/memory/conversation.py +0 -0
  73. {kader-0.1.6 → kader-1.1.0}/kader/memory/session.py +0 -0
  74. {kader-0.1.6 → kader-1.1.0}/kader/memory/state.py +0 -0
  75. {kader-0.1.6 → kader-1.1.0}/kader/prompts/base.py +0 -0
  76. {kader-0.1.6 → kader-1.1.0}/kader/prompts/templates/planning_agent.j2 +0 -0
  77. {kader-0.1.6 → kader-1.1.0}/kader/prompts/templates/react_agent.j2 +0 -0
  78. {kader-0.1.6 → kader-1.1.0}/kader/providers/base.py +0 -0
  79. {kader-0.1.6 → kader-1.1.0}/kader/providers/mock.py +0 -0
  80. {kader-0.1.6 → kader-1.1.0}/kader/tools/README.md +0 -0
  81. {kader-0.1.6 → kader-1.1.0}/kader/tools/base.py +0 -0
  82. {kader-0.1.6 → kader-1.1.0}/kader/tools/exec_commands.py +0 -0
  83. {kader-0.1.6 → kader-1.1.0}/kader/tools/filesystem.py +0 -0
  84. {kader-0.1.6 → kader-1.1.0}/kader/tools/protocol.py +0 -0
  85. {kader-0.1.6 → kader-1.1.0}/kader/tools/rag.py +0 -0
  86. {kader-0.1.6 → kader-1.1.0}/kader/tools/utils.py +0 -0
  87. {kader-0.1.6 → kader-1.1.0}/kader/tools/web.py +0 -0
  88. {kader-0.1.6 → kader-1.1.0}/tests/providers/test_mock.py +0 -0
  89. {kader-0.1.6 → kader-1.1.0}/tests/providers/test_ollama.py +0 -0
  90. {kader-0.1.6 → kader-1.1.0}/tests/providers/test_providers_base.py +0 -0
  91. {kader-0.1.6 → kader-1.1.0}/tests/test_agent_logger.py +0 -0
  92. {kader-0.1.6 → kader-1.1.0}/tests/test_base_agent.py +0 -0
  93. {kader-0.1.6 → kader-1.1.0}/tests/test_file_memory.py +0 -0
  94. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_exec_commands.py +0 -0
  95. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_filesys_tools.py +0 -0
  96. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_rag.py +0 -0
  97. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_tools_base.py +0 -0
  98. {kader-0.1.6 → kader-1.1.0}/tests/tools/test_web.py +0 -0
@@ -1,3 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: kader
3
+ Version: 1.1.0
4
+ Summary: kader coding agent
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aiofiles>=25.1.0
7
+ Requires-Dist: faiss-cpu>=1.9.0
8
+ Requires-Dist: google-genai>=1.61.0
9
+ Requires-Dist: jinja2>=3.1.6
10
+ Requires-Dist: loguru>=0.7.3
11
+ Requires-Dist: ollama>=0.6.1
12
+ Requires-Dist: outdated>=0.2.2
13
+ Requires-Dist: pyyaml>=6.0.3
14
+ Requires-Dist: tenacity>=9.1.2
15
+ Requires-Dist: textual[syntax]>=6.8.0
16
+ Requires-Dist: typing-extensions>=4.15.0
17
+ Requires-Dist: wcmatch>=10.1
18
+ Description-Content-Type: text/markdown
19
+
1
20
  # Kader
2
21
 
3
22
  Kader is an intelligent coding agent designed to assist with software development tasks. It provides a comprehensive framework for building AI-powered agents with advanced reasoning capabilities and tool integration.
@@ -15,6 +34,7 @@ Kader is an intelligent coding agent designed to assist with software developmen
15
34
  - 🔄 **ReAct Agent Framework** - Reasoning and Acting agent architecture
16
35
  - 🗂️ **File System Tools** - Read, write, search, and edit files
17
36
  - 🔍 **Planning Agent** - Task planning and execution capabilities
37
+ - 🤝 **Agent-As-Tool** - Spawn sub-agents for specific tasks with isolated memory
18
38
 
19
39
  ## Installation
20
40
 
@@ -160,6 +180,40 @@ Kader provides several agent types:
160
180
  - **PlanningAgent**: Agent that plans multi-step tasks
161
181
  - **BaseAgent**: Base agent class for creating custom agents
162
182
 
183
+ ### Agent-As-Tool (AgentTool)
184
+
185
+ The `AgentTool` allows you to wrap a `ReActAgent` as a callable tool, enabling agents to spawn sub-agents for specific tasks with isolated memory contexts.
186
+
187
+ ```python
188
+ from kader.tools import AgentTool
189
+
190
+ # Autonomous execution (runs without pausing for confirmation)
191
+ autonomous_agent = AgentTool(
192
+ name="research_agent",
193
+ description="Research topics autonomously",
194
+ interrupt_before_tool=False,
195
+ )
196
+ result = autonomous_agent.execute(task="Find info about topic X")
197
+
198
+ # Interactive execution (pauses for user confirmation before each tool)
199
+ def my_callback(tool_call_dict, llm_content=None):
200
+ user_input = input("Execute? [y/n]: ")
201
+ return (user_input.lower() == 'y', None)
202
+
203
+ interactive_agent = AgentTool(
204
+ name="interactive_agent",
205
+ interrupt_before_tool=True,
206
+ tool_confirmation_callback=my_callback,
207
+ )
208
+ result = interactive_agent.execute(task="Analyze data and generate report")
209
+ ```
210
+
211
+ **Key Features:**
212
+ - Each sub-agent has isolated memory (separate `SlidingWindowConversationManager`)
213
+ - Default tools included: filesystem, web search, command executor
214
+ - Optional `interrupt_before_tool` for user confirmation before tool execution
215
+ - Task completes when the agent returns its final response
216
+
163
217
  ### Memory Management
164
218
 
165
219
  Kader's memory system includes:
@@ -177,6 +231,7 @@ Kader includes a rich set of tools:
177
231
  - **Command Executor**: Execute shell commands safely
178
232
  - **Web Tools**: Search and fetch web content
179
233
  - **RAG Tools**: Retrieval Augmented Generation capabilities
234
+ - **AgentTool**: Spawn sub-agents for specific tasks
180
235
 
181
236
  ## Examples
182
237
 
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: kader
3
- Version: 0.1.6
4
- Summary: kader coding agent
5
- Requires-Python: >=3.11
6
- Requires-Dist: faiss-cpu>=1.9.0
7
- Requires-Dist: jinja2>=3.1.6
8
- Requires-Dist: loguru>=0.7.3
9
- Requires-Dist: ollama>=0.6.1
10
- Requires-Dist: outdated>=0.2.2
11
- Requires-Dist: pyyaml>=6.0.3
12
- Requires-Dist: tenacity>=9.1.2
13
- Requires-Dist: textual[syntax]>=6.8.0
14
- Requires-Dist: typing-extensions>=4.15.0
15
- Requires-Dist: wcmatch>=10.1
16
- Description-Content-Type: text/markdown
17
-
18
1
  # Kader
19
2
 
20
3
  Kader is an intelligent coding agent designed to assist with software development tasks. It provides a comprehensive framework for building AI-powered agents with advanced reasoning capabilities and tool integration.
@@ -32,6 +15,7 @@ Kader is an intelligent coding agent designed to assist with software developmen
32
15
  - 🔄 **ReAct Agent Framework** - Reasoning and Acting agent architecture
33
16
  - 🗂️ **File System Tools** - Read, write, search, and edit files
34
17
  - 🔍 **Planning Agent** - Task planning and execution capabilities
18
+ - 🤝 **Agent-As-Tool** - Spawn sub-agents for specific tasks with isolated memory
35
19
 
36
20
  ## Installation
37
21
 
@@ -177,6 +161,40 @@ Kader provides several agent types:
177
161
  - **PlanningAgent**: Agent that plans multi-step tasks
178
162
  - **BaseAgent**: Base agent class for creating custom agents
179
163
 
164
+ ### Agent-As-Tool (AgentTool)
165
+
166
+ The `AgentTool` allows you to wrap a `ReActAgent` as a callable tool, enabling agents to spawn sub-agents for specific tasks with isolated memory contexts.
167
+
168
+ ```python
169
+ from kader.tools import AgentTool
170
+
171
+ # Autonomous execution (runs without pausing for confirmation)
172
+ autonomous_agent = AgentTool(
173
+ name="research_agent",
174
+ description="Research topics autonomously",
175
+ interrupt_before_tool=False,
176
+ )
177
+ result = autonomous_agent.execute(task="Find info about topic X")
178
+
179
+ # Interactive execution (pauses for user confirmation before each tool)
180
+ def my_callback(tool_call_dict, llm_content=None):
181
+ user_input = input("Execute? [y/n]: ")
182
+ return (user_input.lower() == 'y', None)
183
+
184
+ interactive_agent = AgentTool(
185
+ name="interactive_agent",
186
+ interrupt_before_tool=True,
187
+ tool_confirmation_callback=my_callback,
188
+ )
189
+ result = interactive_agent.execute(task="Analyze data and generate report")
190
+ ```
191
+
192
+ **Key Features:**
193
+ - Each sub-agent has isolated memory (separate `SlidingWindowConversationManager`)
194
+ - Default tools included: filesystem, web search, command executor
195
+ - Optional `interrupt_before_tool` for user confirmation before tool execution
196
+ - Task completes when the agent returns its final response
197
+
180
198
  ### Memory Management
181
199
 
182
200
  Kader's memory system includes:
@@ -194,6 +212,7 @@ Kader includes a rich set of tools:
194
212
  - **Command Executor**: Execute shell commands safely
195
213
  - **Web Tools**: Search and fetch web content
196
214
  - **RAG Tools**: Retrieval Augmented Generation capabilities
215
+ - **AgentTool**: Spawn sub-agents for specific tasks
197
216
 
198
217
  ## Examples
199
218
 
@@ -1,7 +1,9 @@
1
1
  """Kader CLI - Modern Vibe Coding CLI with Textual."""
2
2
 
3
3
  import asyncio
4
+ import atexit
4
5
  import threading
6
+ from concurrent.futures import ThreadPoolExecutor
5
7
  from importlib.metadata import version as get_version
6
8
  from pathlib import Path
7
9
  from typing import Optional
@@ -18,14 +20,13 @@ from textual.widgets import (
18
20
  Tree,
19
21
  )
20
22
 
21
- from kader.agent.agents import ReActAgent
22
23
  from kader.memory import (
23
24
  FileSessionManager,
24
25
  MemoryConfig,
25
- SlidingWindowConversationManager,
26
26
  )
27
- from kader.tools import get_default_registry
27
+ from kader.workflows import PlannerExecutorWorkflow
28
28
 
29
+ from .llm_factory import LLMProviderFactory
29
30
  from .utils import (
30
31
  DEFAULT_MODEL,
31
32
  HELP_TEXT,
@@ -103,22 +104,83 @@ class KaderApp(App):
103
104
  self._model_selector: Optional[ModelSelector] = None
104
105
  self._update_info: Optional[str] = None # Latest version if update available
105
106
 
106
- self._agent = self._create_agent(self._current_model)
107
+ # Dedicated thread pool for agent invocation (isolated from default pool)
108
+ self._agent_executor = ThreadPoolExecutor(
109
+ max_workers=2, thread_name_prefix="kader_agent"
110
+ )
111
+ # Ensure executor is properly shut down on exit
112
+ atexit.register(self._agent_executor.shutdown, wait=False)
113
+
114
+ self._workflow = self._create_workflow(self._current_model)
107
115
 
108
- def _create_agent(self, model_name: str) -> ReActAgent:
109
- """Create a new ReActAgent with the specified model."""
110
- registry = get_default_registry()
111
- memory = SlidingWindowConversationManager(window_size=10)
112
- return ReActAgent(
116
+ def _create_workflow(self, model_name: str) -> PlannerExecutorWorkflow:
117
+ """Create a new PlannerExecutorWorkflow with the specified model."""
118
+ # Create provider using factory (supports provider:model format)
119
+ provider = LLMProviderFactory.create_provider(model_name)
120
+
121
+ return PlannerExecutorWorkflow(
113
122
  name="kader_cli",
114
- tools=registry,
115
- memory=memory,
116
- model_name=model_name,
117
- use_persistence=True,
123
+ provider=provider,
124
+ model_name=model_name, # Keep for reference
118
125
  interrupt_before_tool=True,
119
126
  tool_confirmation_callback=self._tool_confirmation_callback,
127
+ direct_execution_callback=self._direct_execution_callback,
128
+ tool_execution_result_callback=self._tool_execution_result_callback,
129
+ use_persistence=True,
130
+ executor_names=["executor"],
131
+ )
132
+
133
+ def _direct_execution_callback(self, message: str, tool_name: str) -> None:
134
+ """
135
+ Callback for direct execution tools - called from agent thread.
136
+
137
+ Shows a message in the conversation view without blocking for confirmation.
138
+ """
139
+ # Schedule message display on main thread
140
+ self.call_from_thread(self._show_direct_execution_message, message, tool_name)
141
+
142
+ def _show_direct_execution_message(self, message: str, tool_name: str) -> None:
143
+ """Show a direct execution message in the conversation view."""
144
+ try:
145
+ conversation = self.query_one("#conversation-view", ConversationView)
146
+ # User-friendly message showing the tool is executing
147
+ friendly_message = f"[>] Executing {tool_name}..."
148
+ conversation.add_message(friendly_message, "assistant")
149
+ conversation.scroll_end()
150
+ except Exception:
151
+ pass
152
+
153
+ def _tool_execution_result_callback(
154
+ self, tool_name: str, success: bool, result: str
155
+ ) -> None:
156
+ """
157
+ Callback for tool execution results - called from agent thread.
158
+
159
+ Updates the conversation view with the execution result.
160
+ """
161
+ # Schedule result display on main thread
162
+ self.call_from_thread(
163
+ self._show_tool_execution_result, tool_name, success, result
120
164
  )
121
165
 
166
+ def _show_tool_execution_result(
167
+ self, tool_name: str, success: bool, result: str
168
+ ) -> None:
169
+ """Show the tool execution result in the conversation view."""
170
+ try:
171
+ conversation = self.query_one("#conversation-view", ConversationView)
172
+ if success:
173
+ # User-friendly success message
174
+ friendly_message = f"(+) {tool_name} completed successfully"
175
+ else:
176
+ # User-friendly error message with truncated result
177
+ error_preview = result[:100] + "..." if len(result) > 100 else result
178
+ friendly_message = f"(-) {tool_name} failed: {error_preview}"
179
+ conversation.add_message(friendly_message, "assistant")
180
+ conversation.scroll_end()
181
+ except Exception:
182
+ pass
183
+
122
184
  def _tool_confirmation_callback(self, message: str) -> tuple[bool, Optional[str]]:
123
185
  """
124
186
  Callback for tool confirmation - called from agent thread.
@@ -135,7 +197,10 @@ class KaderApp(App):
135
197
 
136
198
  # Wait for user response (blocking in agent thread)
137
199
  # This is safe because we're in a background thread
138
- self._confirmation_event.wait()
200
+ # Timeout after 5 minutes to prevent indefinite blocking
201
+ if not self._confirmation_event.wait(timeout=300):
202
+ # Timeout occurred - decline tool execution gracefully
203
+ return (False, "Tool confirmation timed out after 5 minutes")
139
204
 
140
205
  # Return the result
141
206
  return self._confirmation_result
@@ -183,7 +248,8 @@ class KaderApp(App):
183
248
  if event.confirmed:
184
249
  if tool_message:
185
250
  conversation.add_message(tool_message, "assistant")
186
- conversation.add_message("(+) Executing tool...", "assistant")
251
+ # Show executing message - will be updated by result callback
252
+ conversation.add_message("[>] Executing tool...", "assistant")
187
253
  # Restart spinner
188
254
  try:
189
255
  spinner = self.query_one(LoadingSpinner)
@@ -207,13 +273,12 @@ class KaderApp(App):
207
273
 
208
274
  async def _show_model_selector(self, conversation: ConversationView) -> None:
209
275
  """Show the model selector widget."""
210
- from kader.providers import OllamaProvider
211
-
212
276
  try:
213
- models = OllamaProvider.get_supported_models()
277
+ # Get models from all available providers
278
+ models = LLMProviderFactory.get_flat_model_list()
214
279
  if not models:
215
280
  conversation.add_message(
216
- "## Models (^^)\n\n*No models found. Is Ollama running?*",
281
+ "## Models (^^)\n\n*No models found. Check provider configurations.*",
217
282
  "assistant",
218
283
  )
219
284
  return
@@ -249,7 +314,7 @@ class KaderApp(App):
249
314
  # Update model and recreate agent
250
315
  old_model = self._current_model
251
316
  self._current_model = event.model
252
- self._agent = self._create_agent(self._current_model)
317
+ self._workflow = self._create_workflow(self._current_model)
253
318
 
254
319
  conversation.add_message(
255
320
  f"(+) Model changed from `{old_model}` to `{self._current_model}`",
@@ -431,8 +496,8 @@ Please resize your terminal."""
431
496
  await self._show_model_selector(conversation)
432
497
  elif cmd == "/clear":
433
498
  conversation.clear_messages()
434
- self._agent.memory.clear()
435
- self._agent.provider.reset_tracking() # Reset usage/cost tracking
499
+ self._workflow.planner.memory.clear()
500
+ self._workflow.planner.provider.reset_tracking() # Reset usage/cost tracking
436
501
  self._current_session_id = None
437
502
  self.notify("Conversation cleared!", severity="information")
438
503
  elif cmd == "/save":
@@ -462,7 +527,7 @@ Please resize your terminal."""
462
527
  )
463
528
 
464
529
  async def _handle_chat(self, message: str) -> None:
465
- """Handle regular chat messages with ReActAgent."""
530
+ """Handle regular chat messages with PlannerExecutorWorkflow."""
466
531
  if self._is_processing:
467
532
  self.notify("Please wait for the current response...", severity="warning")
468
533
  return
@@ -490,20 +555,25 @@ Please resize your terminal."""
490
555
  spinner = self.query_one(LoadingSpinner)
491
556
 
492
557
  try:
493
- # Run the agent invoke in a thread
558
+ # Run the workflow in a dedicated thread pool
494
559
  loop = asyncio.get_event_loop()
495
560
  response = await loop.run_in_executor(
496
- None, lambda: self._agent.invoke(message)
561
+ self._agent_executor, lambda: self._workflow.run(message)
497
562
  )
498
563
 
499
564
  # Hide spinner and show response (this runs on main thread via await)
500
565
  spinner.stop()
501
- if response and response.content:
502
- conversation.add_message(response.content, "assistant")
566
+ if response:
567
+ conversation.add_message(
568
+ response,
569
+ "assistant",
570
+ model_name=self._workflow.planner.provider.model,
571
+ usage_cost=self._workflow.planner.provider.total_cost.total_cost,
572
+ )
503
573
 
504
574
  except Exception as e:
505
575
  spinner.stop()
506
- error_msg = f"(-) **Error:** {str(e)}\n\nMake sure Ollama is running and the model `{self._current_model}` is available."
576
+ error_msg = f"(-) **Error:** {str(e)}\n\nMake sure the provider for `{self._current_model}` is configured and available."
507
577
  conversation.add_message(error_msg, "assistant")
508
578
  self.notify(f"Error: {e}", severity="error")
509
579
 
@@ -516,7 +586,7 @@ Please resize your terminal."""
516
586
  """Clear the conversation (Ctrl+L)."""
517
587
  conversation = self.query_one("#conversation-view", ConversationView)
518
588
  conversation.clear_messages()
519
- self._agent.memory.clear()
589
+ self._workflow.planner.memory.clear()
520
590
  self.notify("Conversation cleared!", severity="information")
521
591
 
522
592
  def action_save_session(self) -> None:
@@ -548,8 +618,10 @@ Please resize your terminal."""
548
618
  session = self._session_manager.create_session("kader_cli")
549
619
  self._current_session_id = session.session_id
550
620
 
551
- # Get messages from agent memory and save
552
- messages = [msg.message for msg in self._agent.memory.get_messages()]
621
+ # Get messages from planner memory and save
622
+ messages = [
623
+ msg.message for msg in self._workflow.planner.memory.get_messages()
624
+ ]
553
625
  self._session_manager.save_conversation(self._current_session_id, messages)
554
626
 
555
627
  conversation.add_message(
@@ -580,11 +652,11 @@ Please resize your terminal."""
580
652
 
581
653
  # Clear current state
582
654
  conversation.clear_messages()
583
- self._agent.memory.clear()
655
+ self._workflow.planner.memory.clear()
584
656
 
585
657
  # Add loaded messages to memory and UI
586
658
  for msg in messages:
587
- self._agent.memory.add_message(msg)
659
+ self._workflow.planner.memory.add_message(msg)
588
660
  role = msg.get("role", "user")
589
661
  content = msg.get("content", "")
590
662
  if role in ["user", "assistant"] and content:
@@ -633,9 +705,9 @@ Please resize your terminal."""
633
705
  """Display LLM usage costs."""
634
706
  try:
635
707
  # Get cost and usage from the provider
636
- cost = self._agent.provider.total_cost
637
- usage = self._agent.provider.total_usage
638
- model = self._agent.provider.model
708
+ cost = self._workflow.planner.provider.total_cost
709
+ usage = self._workflow.planner.provider.total_usage
710
+ model = self._workflow.planner.provider.model
639
711
 
640
712
  lines = [
641
713
  "## Usage Costs ($)\n",
@@ -132,6 +132,26 @@ ConversationView {
132
132
  scrollbar-size: 1 1;
133
133
  }
134
134
 
135
+ .message-footer {
136
+ height: auto;
137
+ margin-top: 0;
138
+ padding: 0 1;
139
+ border-top: none;
140
+ }
141
+
142
+ .footer-left {
143
+ color: $secondary;
144
+ text-style: italic;
145
+ width: 1fr;
146
+ }
147
+
148
+ .footer-right {
149
+ color: $success;
150
+ text-style: bold;
151
+ text-align: right;
152
+ width: auto;
153
+ }
154
+
135
155
  /* ===== Welcome Message ===== */
136
156
 
137
157
  #welcome {
@@ -0,0 +1,165 @@
1
+ """LLM Provider Factory for Kader CLI.
2
+
3
+ Factory pattern implementation for creating LLM provider instances
4
+ with automatic provider detection based on model name format.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from kader.providers import GoogleProvider, OllamaProvider
10
+ from kader.providers.base import BaseLLMProvider, ModelConfig
11
+
12
+
13
+ class LLMProviderFactory:
14
+ """
15
+ Factory for creating LLM provider instances.
16
+
17
+ Supports multiple providers with automatic detection based on model name format.
18
+ Model names can be specified as:
19
+ - "provider:model" (e.g., "google:gemini-2.5-flash", "ollama:kimi-k2.5:cloud")
20
+ - "model" (defaults to Ollama for backward compatibility)
21
+
22
+ Example:
23
+ factory = LLMProviderFactory()
24
+ provider = factory.create_provider("google:gemini-2.5-flash")
25
+
26
+ # Or with default provider (Ollama)
27
+ provider = factory.create_provider("kimi-k2.5:cloud")
28
+ """
29
+
30
+ # Registered provider classes
31
+ PROVIDERS: dict[str, type[BaseLLMProvider]] = {
32
+ "ollama": OllamaProvider,
33
+ "google": GoogleProvider,
34
+ }
35
+
36
+ # Default provider when no prefix is specified
37
+ DEFAULT_PROVIDER = "ollama"
38
+
39
+ @classmethod
40
+ def parse_model_name(cls, model_string: str) -> tuple[str, str]:
41
+ """
42
+ Parse model string to extract provider and model name.
43
+
44
+ Args:
45
+ model_string: Model string in format "provider:model" or just "model"
46
+
47
+ Returns:
48
+ Tuple of (provider_name, model_name)
49
+ """
50
+ # Check if the string starts with a known provider prefix
51
+ for provider_name in cls.PROVIDERS.keys():
52
+ prefix = f"{provider_name}:"
53
+ if model_string.lower().startswith(prefix):
54
+ return provider_name, model_string[len(prefix) :]
55
+
56
+ # No known provider prefix found, use default
57
+ return cls.DEFAULT_PROVIDER, model_string
58
+
59
+ @classmethod
60
+ def create_provider(
61
+ cls,
62
+ model_string: str,
63
+ config: Optional[ModelConfig] = None,
64
+ ) -> BaseLLMProvider:
65
+ """
66
+ Create an LLM provider instance.
67
+
68
+ Args:
69
+ model_string: Model identifier (e.g., "google:gemini-2.5-flash" or "kimi-k2.5:cloud")
70
+ config: Optional model configuration
71
+
72
+ Returns:
73
+ Configured provider instance
74
+
75
+ Raises:
76
+ ValueError: If provider is not supported
77
+ """
78
+ provider_name, model_name = cls.parse_model_name(model_string)
79
+
80
+ provider_class = cls.PROVIDERS.get(provider_name)
81
+ if not provider_class:
82
+ supported = ", ".join(cls.PROVIDERS.keys())
83
+ raise ValueError(
84
+ f"Unknown provider: {provider_name}. Supported: {supported}"
85
+ )
86
+
87
+ return provider_class(model=model_name, default_config=config)
88
+
89
+ @classmethod
90
+ def get_all_models(cls) -> dict[str, list[str]]:
91
+ """
92
+ Get all available models from all registered providers.
93
+
94
+ Returns:
95
+ Dictionary mapping provider names to their available models
96
+ (with provider prefix included in model names)
97
+ """
98
+ models: dict[str, list[str]] = {}
99
+
100
+ # Get Ollama models
101
+ try:
102
+ ollama_models = OllamaProvider.get_supported_models()
103
+ models["ollama"] = [f"ollama:{m}" for m in ollama_models]
104
+ except Exception:
105
+ models["ollama"] = []
106
+
107
+ # Get Google models
108
+ try:
109
+ google_models = GoogleProvider.get_supported_models()
110
+ models["google"] = [f"google:{m}" for m in google_models]
111
+ except Exception:
112
+ models["google"] = []
113
+
114
+ return models
115
+
116
+ @classmethod
117
+ def get_flat_model_list(cls) -> list[str]:
118
+ """
119
+ Get a flattened list of all available models with provider prefixes.
120
+
121
+ Returns:
122
+ List of model strings in "provider:model" format
123
+ """
124
+ all_models = cls.get_all_models()
125
+ flat_list: list[str] = []
126
+ for models in all_models.values():
127
+ flat_list.extend(models)
128
+ return flat_list
129
+
130
+ @classmethod
131
+ def is_provider_available(cls, provider_name: str) -> bool:
132
+ """
133
+ Check if a provider is available and configured.
134
+
135
+ Args:
136
+ provider_name: Name of the provider to check
137
+
138
+ Returns:
139
+ True if provider is available and has models, False otherwise
140
+ """
141
+ provider_name = provider_name.lower()
142
+ if provider_name not in cls.PROVIDERS:
143
+ return False
144
+
145
+ # Try to get models to verify provider is working
146
+ try:
147
+ provider_class = cls.PROVIDERS[provider_name]
148
+ models = provider_class.get_supported_models()
149
+ return len(models) > 0
150
+ except Exception:
151
+ return False
152
+
153
+ @classmethod
154
+ def get_provider_name(cls, model_string: str) -> str:
155
+ """
156
+ Get the provider name for a given model string.
157
+
158
+ Args:
159
+ model_string: Model string in format "provider:model" or just "model"
160
+
161
+ Returns:
162
+ Provider name (e.g., "ollama", "google")
163
+ """
164
+ provider_name, _ = cls.parse_model_name(model_string)
165
+ return provider_name
@@ -1,9 +1,9 @@
1
1
  """Utility constants and helpers for Kader CLI."""
2
2
 
3
- from kader.providers import OllamaProvider
3
+ from .llm_factory import LLMProviderFactory
4
4
 
5
- # Default model
6
- DEFAULT_MODEL = "qwen3-coder:480b-cloud"
5
+ # Default model (with provider prefix for clarity)
6
+ DEFAULT_MODEL = "ollama:kimi-k2.5:cloud"
7
7
 
8
8
  HELP_TEXT = """## Kader CLI Commands
9
9
 
@@ -40,24 +40,32 @@ HELP_TEXT = """## Kader CLI Commands
40
40
  ### Tips:
41
41
  - Type any question to chat with the AI
42
42
  - Use **Tab** to navigate between panels
43
+ - Model format: `provider:model` (e.g., `google:gemini-2.5-flash`)
43
44
  """
44
45
 
45
46
 
46
47
  def get_models_text() -> str:
47
- """Get formatted text of available Ollama models."""
48
+ """Get formatted text of available models from all providers."""
48
49
  try:
49
- models = OllamaProvider.get_supported_models()
50
- if not models:
51
- return "## Available Models (^^)\n\n*No models found. Is Ollama running?*"
50
+ all_models = LLMProviderFactory.get_all_models()
51
+ flat_list = LLMProviderFactory.get_flat_model_list()
52
+
53
+ if not flat_list:
54
+ return "## Available Models (^^)\n\n*No models found. Check provider configurations.*"
52
55
 
53
56
  lines = [
54
57
  "## Available Models (^^)\n",
55
- "| Model | Status |",
56
- "|-------|--------|",
58
+ "| Provider | Model | Status |",
59
+ "|----------|-------|--------|",
57
60
  ]
58
- for model in models:
59
- lines.append(f"| {model} | (+) Available |")
61
+ for provider_name, provider_models in all_models.items():
62
+ for model in provider_models:
63
+ lines.append(f"| {provider_name.title()} | `{model}` | (+) Available |")
64
+
60
65
  lines.append(f"\n*Currently using: **{DEFAULT_MODEL}***")
66
+ lines.append(
67
+ "\n> (!) Tip: Use `provider:model` format (e.g., `google:gemini-2.5-flash`)"
68
+ )
61
69
  return "\n".join(lines)
62
70
  except Exception as e:
63
71
  return f"## Available Models (^^)\n\n*Error fetching models: {e}*"