alita-sdk 0.3.458__py3-none-any.whl → 0.3.460__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,1055 @@
1
+ """
2
+ Agent commands for Alita CLI.
3
+
4
+ Provides commands to work with agents interactively or in handoff mode,
5
+ supporting both platform agents and local agent definition files.
6
+ """
7
+
8
+ import click
9
+ import json
10
+ import logging
11
+ import sqlite3
12
+ from typing import Optional, Dict, Any, List
13
+ from pathlib import Path
14
+ import yaml
15
+
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.markdown import Markdown
20
+ from rich import box
21
+ from rich.text import Text
22
+ from rich.status import Status
23
+ from rich.live import Live
24
+
25
+ from .cli import get_client
26
+ from .config import substitute_env_vars
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Create a rich console for beautiful output
31
+ console = Console()
32
+
33
+
34
+ def _print_banner(agent_name: str, agent_type: str = "local"):
35
+ """Print a nice banner for the chat session using rich."""
36
+ content = Text()
37
+ content.append("🤖 ALITA AGENT CHAT\n\n", style="bold cyan")
38
+ content.append(f"Agent: ", style="bold")
39
+ content.append(f"{agent_name}\n", style="cyan")
40
+ content.append(f"Type: ", style="bold")
41
+ content.append(f"{agent_type}", style="cyan")
42
+
43
+ panel = Panel(
44
+ content,
45
+ box=box.DOUBLE,
46
+ border_style="cyan",
47
+ padding=(1, 2)
48
+ )
49
+ console.print(panel)
50
+
51
+
52
+ def _print_help():
53
+ """Print help message with commands using rich table."""
54
+ table = Table(
55
+ show_header=True,
56
+ header_style="bold yellow",
57
+ border_style="yellow",
58
+ box=box.ROUNDED,
59
+ title="Commands",
60
+ title_style="bold yellow"
61
+ )
62
+
63
+ table.add_column("Command", style="cyan", no_wrap=True)
64
+ table.add_column("Description", style="white")
65
+
66
+ table.add_row("/clear", "Clear conversation history")
67
+ table.add_row("/history", "Show conversation history")
68
+ table.add_row("/save", "Save conversation to file")
69
+ table.add_row("/help", "Show this help")
70
+ table.add_row("exit/quit", "End conversation")
71
+ table.add_row("", "")
72
+ table.add_row("@", "Mention files")
73
+ table.add_row("/", "Run commands")
74
+
75
+ console.print(table)
76
+
77
+
78
+ def load_agent_definition(file_path: str) -> Dict[str, Any]:
79
+ """
80
+ Load agent definition from file.
81
+
82
+ Supports:
83
+ - YAML files (.yaml, .yml)
84
+ - JSON files (.json)
85
+ - Markdown files with YAML frontmatter (.md)
86
+
87
+ Args:
88
+ file_path: Path to agent definition file
89
+
90
+ Returns:
91
+ Dictionary with agent configuration
92
+ """
93
+ path = Path(file_path)
94
+
95
+ if not path.exists():
96
+ raise FileNotFoundError(f"Agent definition not found: {file_path}")
97
+
98
+ content = path.read_text()
99
+
100
+ # Handle markdown with YAML frontmatter
101
+ if path.suffix == '.md':
102
+ if content.startswith('---'):
103
+ parts = content.split('---', 2)
104
+ if len(parts) >= 3:
105
+ frontmatter = yaml.safe_load(parts[1])
106
+ system_prompt = parts[2].strip()
107
+
108
+ # Apply environment variable substitution
109
+ system_prompt = substitute_env_vars(system_prompt)
110
+
111
+ return {
112
+ 'name': frontmatter.get('name', path.stem),
113
+ 'description': frontmatter.get('description', ''),
114
+ 'system_prompt': system_prompt,
115
+ 'model': frontmatter.get('model'),
116
+ 'tools': frontmatter.get('tools', []),
117
+ 'temperature': frontmatter.get('temperature'),
118
+ 'max_tokens': frontmatter.get('max_tokens'),
119
+ 'toolkit_configs': frontmatter.get('toolkit_configs', []),
120
+ }
121
+
122
+ # Plain markdown - use content as system prompt
123
+ return {
124
+ 'name': path.stem,
125
+ 'system_prompt': substitute_env_vars(content),
126
+ }
127
+
128
+ # Handle YAML
129
+ if path.suffix in ['.yaml', '.yml']:
130
+ content = substitute_env_vars(content)
131
+ config = yaml.safe_load(content)
132
+ if 'system_prompt' in config:
133
+ config['system_prompt'] = substitute_env_vars(config['system_prompt'])
134
+ return config
135
+
136
+ # Handle JSON
137
+ if path.suffix == '.json':
138
+ content = substitute_env_vars(content)
139
+ config = json.loads(content)
140
+ if 'system_prompt' in config:
141
+ config['system_prompt'] = substitute_env_vars(config['system_prompt'])
142
+ return config
143
+
144
+ raise ValueError(f"Unsupported file format: {path.suffix}")
145
+
146
+
147
+ def load_toolkit_config(file_path: str) -> Dict[str, Any]:
148
+ """Load toolkit configuration from JSON file with env var substitution."""
149
+ path = Path(file_path)
150
+
151
+ if not path.exists():
152
+ raise FileNotFoundError(f"Toolkit configuration not found: {file_path}")
153
+
154
+ with open(path) as f:
155
+ content = f.read()
156
+
157
+ # Apply environment variable substitution
158
+ content = substitute_env_vars(content)
159
+ return json.loads(content)
160
+
161
+
162
+ def _select_agent_interactive(client, config) -> Optional[str]:
163
+ """
164
+ Show interactive menu to select an agent from platform and local agents.
165
+
166
+ Returns:
167
+ Agent source (name/id for platform, file path for local) or None if cancelled
168
+ """
169
+ from .config import CLIConfig
170
+
171
+ console.print("\n🤖 [bold cyan]Select an agent to chat with:[/bold cyan]\n")
172
+
173
+ agents_list = []
174
+
175
+ # Load platform agents
176
+ try:
177
+ platform_agents = client.get_list_of_apps()
178
+ for agent in platform_agents:
179
+ agents_list.append({
180
+ 'type': 'platform',
181
+ 'name': agent['name'],
182
+ 'source': agent['name'],
183
+ 'description': agent.get('description', '')[:60]
184
+ })
185
+ except Exception as e:
186
+ logger.debug(f"Failed to load platform agents: {e}")
187
+
188
+ # Load local agents
189
+ agents_dir = config.agents_dir
190
+ search_dir = Path(agents_dir)
191
+
192
+ if search_dir.exists():
193
+ for pattern in ['*.agent.md', '*.agent.yaml', '*.agent.yml', '*.agent.json']:
194
+ for file_path in search_dir.rglob(pattern):
195
+ try:
196
+ agent_def = load_agent_definition(str(file_path))
197
+ agents_list.append({
198
+ 'type': 'local',
199
+ 'name': agent_def.get('name', file_path.stem),
200
+ 'source': str(file_path),
201
+ 'description': agent_def.get('description', '')[:60]
202
+ })
203
+ except Exception as e:
204
+ logger.debug(f"Failed to load {file_path}: {e}")
205
+
206
+ if not agents_list:
207
+ console.print("[yellow]No agents found. Create an agent first or check your configuration.[/yellow]")
208
+ return None
209
+
210
+ # Display agents with numbers using rich
211
+ for i, agent in enumerate(agents_list, 1):
212
+ agent_type = "📦 Platform" if agent['type'] == 'platform' else "📁 Local"
213
+ console.print(f"{i}. [[bold]{agent_type}[/bold]] [cyan]{agent['name']}[/cyan]")
214
+ if agent['description']:
215
+ console.print(f" [dim]{agent['description']}[/dim]")
216
+
217
+ console.print(f"\n[dim]0. Cancel[/dim]")
218
+
219
+ # Get user selection
220
+ while True:
221
+ try:
222
+ choice = input("\nSelect agent number: ").strip()
223
+
224
+ if choice == '0':
225
+ return None
226
+
227
+ idx = int(choice) - 1
228
+ if 0 <= idx < len(agents_list):
229
+ selected = agents_list[idx]
230
+ console.print(f"\n✓ [green]Selected:[/green] [bold]{selected['name']}[/bold]")
231
+ return selected['source']
232
+ else:
233
+ console.print(f"[yellow]Invalid selection. Please enter a number between 0 and {len(agents_list)}[/yellow]")
234
+ except ValueError:
235
+ console.print("[yellow]Please enter a valid number[/yellow]")
236
+ except (KeyboardInterrupt, EOFError):
237
+ console.print("\n\n[dim]Cancelled.[/dim]")
238
+ return None
239
+
240
+
241
+ @click.group()
242
+ def agent():
243
+ """Agent testing and interaction commands."""
244
+ pass
245
+
246
+
247
+ @agent.command('list')
248
+ @click.option('--local', is_flag=True, help='List local agent definition files')
249
+ @click.option('--directory', default=None, help='Directory to search for local agents (defaults to AGENTS_DIR from .env)')
250
+ @click.pass_context
251
+ def agent_list(ctx, local: bool, directory: Optional[str]):
252
+ """
253
+ List available agents.
254
+
255
+ By default, lists agents from the platform.
256
+ Use --local to list agent definition files in the local directory.
257
+ """
258
+ formatter = ctx.obj['formatter']
259
+ config = ctx.obj['config']
260
+
261
+ try:
262
+ if local:
263
+ # List local agent definition files
264
+ if directory is None:
265
+ directory = config.agents_dir
266
+ search_dir = Path(directory)
267
+
268
+ if not search_dir.exists():
269
+ console.print(f"[red]Directory not found: {directory}[/red]")
270
+ return
271
+
272
+ agents = []
273
+
274
+ # Find agent definition files
275
+ for pattern in ['*.agent.md', '*.agent.yaml', '*.agent.yml', '*.agent.json']:
276
+ for file_path in search_dir.rglob(pattern):
277
+ try:
278
+ agent_def = load_agent_definition(str(file_path))
279
+ # Use relative path if already relative, otherwise make it relative to cwd
280
+ try:
281
+ display_path = str(file_path.relative_to(Path.cwd()))
282
+ except ValueError:
283
+ display_path = str(file_path)
284
+
285
+ agents.append({
286
+ 'name': agent_def.get('name', file_path.stem),
287
+ 'file': display_path,
288
+ 'description': agent_def.get('description', '')[:80]
289
+ })
290
+ except Exception as e:
291
+ logger.debug(f"Failed to load {file_path}: {e}")
292
+
293
+ if not agents:
294
+ console.print(f"\n[yellow]No agent definition files found in {directory}[/yellow]")
295
+ return
296
+
297
+ # Display local agents in a table
298
+ table = Table(
299
+ title=f"Local Agent Definitions in {directory}",
300
+ show_header=True,
301
+ header_style="bold cyan",
302
+ border_style="cyan",
303
+ box=box.ROUNDED
304
+ )
305
+ table.add_column("Name", style="bold cyan", no_wrap=True)
306
+ table.add_column("File", style="dim")
307
+ table.add_column("Description", style="white")
308
+
309
+ for agent_info in sorted(agents, key=lambda x: x['name']):
310
+ table.add_row(
311
+ agent_info['name'],
312
+ agent_info['file'],
313
+ agent_info['description'] or "-"
314
+ )
315
+
316
+ console.print("\n")
317
+ console.print(table)
318
+ console.print(f"\n[green]Total: {len(agents)} local agents[/green]")
319
+
320
+ else:
321
+ # List platform agents
322
+ client = get_client(ctx)
323
+
324
+ agents = client.get_list_of_apps()
325
+
326
+ if formatter.__class__.__name__ == 'JSONFormatter':
327
+ click.echo(formatter._dump({'agents': agents, 'total': len(agents)}))
328
+ else:
329
+ table = Table(
330
+ title="Available Platform Agents",
331
+ show_header=True,
332
+ header_style="bold cyan",
333
+ border_style="cyan",
334
+ box=box.ROUNDED
335
+ )
336
+ table.add_column("ID", style="yellow", no_wrap=True)
337
+ table.add_column("Name", style="bold cyan")
338
+ table.add_column("Description", style="white")
339
+
340
+ for agent_info in agents:
341
+ table.add_row(
342
+ str(agent_info['id']),
343
+ agent_info['name'],
344
+ agent_info.get('description', '')[:80] or "-"
345
+ )
346
+
347
+ console.print("\n")
348
+ console.print(table)
349
+ console.print(f"\n[green]Total: {len(agents)} agents[/green]")
350
+
351
+ except Exception as e:
352
+ logger.exception("Failed to list agents")
353
+ error_panel = Panel(
354
+ str(e),
355
+ title="Error",
356
+ border_style="red",
357
+ box=box.ROUNDED
358
+ )
359
+ console.print(error_panel, style="red")
360
+ raise click.Abort()
361
+
362
+
363
+ @agent.command('show')
364
+ @click.argument('agent_source')
365
+ @click.option('--version', help='Agent version (for platform agents)')
366
+ @click.pass_context
367
+ def agent_show(ctx, agent_source: str, version: Optional[str]):
368
+ """
369
+ Show agent details.
370
+
371
+ AGENT_SOURCE can be:
372
+ - Platform agent ID or name (e.g., "123" or "my-agent")
373
+ - Path to local agent file (e.g., ".github/agents/sdk-dev.agent.md")
374
+ """
375
+ formatter = ctx.obj['formatter']
376
+
377
+ try:
378
+ # Check if it's a file path
379
+ if Path(agent_source).exists():
380
+ # Local agent file
381
+ agent_def = load_agent_definition(agent_source)
382
+
383
+ if formatter.__class__.__name__ == 'JSONFormatter':
384
+ click.echo(formatter._dump(agent_def))
385
+ else:
386
+ # Create details panel
387
+ details = Text()
388
+ details.append("File: ", style="bold")
389
+ details.append(f"{agent_source}\n", style="cyan")
390
+
391
+ if agent_def.get('description'):
392
+ details.append("\nDescription: ", style="bold")
393
+ details.append(f"{agent_def['description']}\n", style="white")
394
+
395
+ if agent_def.get('model'):
396
+ details.append("Model: ", style="bold")
397
+ details.append(f"{agent_def['model']}\n", style="cyan")
398
+
399
+ if agent_def.get('tools'):
400
+ details.append("Tools: ", style="bold")
401
+ details.append(f"{', '.join(agent_def['tools'])}\n", style="cyan")
402
+
403
+ if agent_def.get('temperature') is not None:
404
+ details.append("Temperature: ", style="bold")
405
+ details.append(f"{agent_def['temperature']}\n", style="cyan")
406
+
407
+ panel = Panel(
408
+ details,
409
+ title=f"Local Agent: {agent_def.get('name', 'Unknown')}",
410
+ title_align="left",
411
+ border_style="cyan",
412
+ box=box.ROUNDED
413
+ )
414
+ console.print("\n")
415
+ console.print(panel)
416
+
417
+ if agent_def.get('system_prompt'):
418
+ console.print("\n[bold]System Prompt:[/bold]")
419
+ console.print(Panel(agent_def['system_prompt'][:500] + "...", border_style="dim", box=box.ROUNDED))
420
+
421
+ else:
422
+ # Platform agent
423
+ client = get_client(ctx)
424
+
425
+ # Try to find agent by ID or name
426
+ agents = client.get_list_of_apps()
427
+
428
+ agent = None
429
+ try:
430
+ agent_id = int(agent_source)
431
+ agent = next((a for a in agents if a['id'] == agent_id), None)
432
+ except ValueError:
433
+ agent = next((a for a in agents if a['name'] == agent_source), None)
434
+
435
+ if not agent:
436
+ raise click.ClickException(f"Agent '{agent_source}' not found")
437
+
438
+ # Get details
439
+ details = client.get_app_details(agent['id'])
440
+
441
+ if formatter.__class__.__name__ == 'JSONFormatter':
442
+ click.echo(formatter._dump(details))
443
+ else:
444
+ # Create platform agent details panel
445
+ content = Text()
446
+ content.append("ID: ", style="bold")
447
+ content.append(f"{details['id']}\n", style="yellow")
448
+
449
+ if details.get('description'):
450
+ content.append("\nDescription: ", style="bold")
451
+ content.append(f"{details['description']}\n", style="white")
452
+
453
+ panel = Panel(
454
+ content,
455
+ title=f"Agent: {details['name']}",
456
+ title_align="left",
457
+ border_style="cyan",
458
+ box=box.ROUNDED
459
+ )
460
+ console.print("\n")
461
+ console.print(panel)
462
+
463
+ # Display versions in a table
464
+ if details.get('versions'):
465
+ console.print("\n[bold]Versions:[/bold]")
466
+ versions_table = Table(box=box.ROUNDED, border_style="dim")
467
+ versions_table.add_column("Name", style="cyan")
468
+ versions_table.add_column("ID", style="yellow")
469
+ for ver in details.get('versions', []):
470
+ versions_table.add_row(ver['name'], str(ver['id']))
471
+ console.print(versions_table)
472
+
473
+ except click.ClickException:
474
+ raise
475
+ except Exception as e:
476
+ logger.exception("Failed to show agent details")
477
+ error_panel = Panel(
478
+ str(e),
479
+ title="Error",
480
+ border_style="red",
481
+ box=box.ROUNDED
482
+ )
483
+ console.print(error_panel, style="red")
484
+ raise click.Abort()
485
+
486
+
487
+ @agent.command('chat')
488
+ @click.argument('agent_source', required=False)
489
+ @click.option('--version', help='Agent version (for platform agents)')
490
+ @click.option('--toolkit-config', multiple=True, type=click.Path(exists=True),
491
+ help='Toolkit configuration files (can specify multiple)')
492
+ @click.option('--thread-id', help='Continue existing conversation thread')
493
+ @click.option('--model', help='Override LLM model')
494
+ @click.option('--temperature', type=float, help='Override temperature')
495
+ @click.option('--max-tokens', type=int, help='Override max tokens')
496
+ @click.pass_context
497
+ def agent_chat(ctx, agent_source: Optional[str], version: Optional[str],
498
+ toolkit_config: tuple, thread_id: Optional[str],
499
+ model: Optional[str], temperature: Optional[float],
500
+ max_tokens: Optional[int]):
501
+ """
502
+ Start interactive chat with an agent.
503
+
504
+ If AGENT_SOURCE is not provided, shows an interactive menu to select from
505
+ available agents (both platform and local).
506
+
507
+ AGENT_SOURCE can be:
508
+ - Platform agent ID or name
509
+ - Path to local agent file
510
+
511
+ Examples:
512
+
513
+ # Interactive selection
514
+ alita-cli agent chat
515
+
516
+ # Chat with platform agent
517
+ alita-cli agent chat my-agent
518
+
519
+ # Chat with local agent
520
+ alita-cli agent chat .github/agents/sdk-dev.agent.md
521
+
522
+ # With toolkit configurations
523
+ alita-cli agent chat my-agent \\
524
+ --toolkit-config jira-config.json \\
525
+ --toolkit-config github-config.json
526
+
527
+ # Continue previous conversation
528
+ alita-cli agent chat my-agent --thread-id abc123
529
+ """
530
+ formatter = ctx.obj['formatter']
531
+ config = ctx.obj['config']
532
+ client = get_client(ctx)
533
+
534
+ try:
535
+ # If no agent specified, show selection menu
536
+ if not agent_source:
537
+ agent_source = _select_agent_interactive(client, config)
538
+ if not agent_source:
539
+ console.print("[yellow]No agent selected. Exiting.[/yellow]")
540
+ return
541
+
542
+ # Load agent
543
+ is_local = Path(agent_source).exists()
544
+
545
+ if is_local:
546
+ agent_def = load_agent_definition(agent_source)
547
+ agent_name = agent_def.get('name', Path(agent_source).stem)
548
+ agent_type = "Local Agent"
549
+ else:
550
+ # Platform agent - find it
551
+ agents = client.get_list_of_apps()
552
+ agent = None
553
+
554
+ try:
555
+ agent_id = int(agent_source)
556
+ agent = next((a for a in agents if a['id'] == agent_id), None)
557
+ except ValueError:
558
+ agent = next((a for a in agents if a['name'] == agent_source), None)
559
+
560
+ if not agent:
561
+ raise click.ClickException(f"Agent '{agent_source}' not found")
562
+
563
+ agent_name = agent['name']
564
+ agent_type = "Platform Agent"
565
+
566
+ # Print nice banner
567
+ _print_banner(agent_name, agent_type)
568
+ _print_help()
569
+
570
+ # Initialize conversation
571
+ chat_history = []
572
+
573
+ # Create memory for agent
574
+ from langgraph.checkpoint.sqlite import SqliteSaver
575
+ memory = SqliteSaver(sqlite3.connect(":memory:", check_same_thread=False))
576
+
577
+ # Load toolkits if provided
578
+ toolkit_configs = []
579
+
580
+ # First load from agent definition if local
581
+ if is_local and 'toolkit_configs' in agent_def:
582
+ for tk_config in agent_def['toolkit_configs']:
583
+ if isinstance(tk_config, dict):
584
+ if 'file' in tk_config:
585
+ # Load from file
586
+ config = load_toolkit_config(tk_config['file'])
587
+ toolkit_configs.append(config)
588
+ console.print(f"[dim]Loaded toolkit config from agent definition: {tk_config['file']}[/dim]")
589
+ elif 'config' in tk_config:
590
+ # Inline config
591
+ toolkit_configs.append(tk_config['config'])
592
+ console.print(f"[dim]Loaded inline toolkit config: {tk_config['config'].get('toolkit_name', 'unknown')}[/dim]")
593
+
594
+ # Then load from --toolkit-config options
595
+ if toolkit_config:
596
+ for config_path in toolkit_config:
597
+ config = load_toolkit_config(config_path)
598
+ toolkit_configs.append(config)
599
+ console.print(f"[dim]Loaded toolkit config: {config_path}[/dim]")
600
+
601
+ # Auto-add toolkits to tools if not already present
602
+ if is_local and toolkit_configs:
603
+ tools = agent_def.get('tools', [])
604
+ for tk_config in toolkit_configs:
605
+ toolkit_name = tk_config.get('toolkit_name')
606
+ if toolkit_name and toolkit_name not in tools:
607
+ tools.append(toolkit_name)
608
+ console.print(f"[dim]Auto-added toolkit to tools: {toolkit_name}[/dim]")
609
+ agent_def['tools'] = tools
610
+
611
+ # Create agent executor
612
+ if is_local:
613
+ # For local agents, use direct LLM integration
614
+ llm_model = model or agent_def.get('model', 'gpt-4o')
615
+ llm_temperature = temperature if temperature is not None else agent_def.get('temperature', 0.7)
616
+ llm_max_tokens = max_tokens or agent_def.get('max_tokens', 2000)
617
+
618
+ system_prompt = agent_def.get('system_prompt', '')
619
+
620
+ # Display configuration
621
+ console.print()
622
+ console.print(f"✓ [green]Using model:[/green] [bold]{llm_model}[/bold]")
623
+ console.print(f"✓ [green]Temperature:[/green] [bold]{llm_temperature}[/bold]")
624
+ if agent_def.get('tools'):
625
+ console.print(f"✓ [green]Tools:[/green] [bold]{', '.join(agent_def['tools'])}[/bold]")
626
+ console.print()
627
+
628
+ # Create LLM instance using AlitaClient
629
+ try:
630
+ llm = client.get_llm(
631
+ model_name=llm_model,
632
+ model_config={
633
+ 'temperature': llm_temperature,
634
+ 'max_tokens': llm_max_tokens
635
+ }
636
+ )
637
+ except Exception as e:
638
+ console.print(f"\n✗ [red]Failed to create LLM instance:[/red] {e}")
639
+ console.print("[yellow]Hint: Make sure OPENAI_API_KEY or other LLM credentials are set[/yellow]")
640
+ return
641
+
642
+ agent_executor = None # Local agents use direct LLM calls
643
+ else:
644
+ # Platform agent
645
+ details = client.get_app_details(agent['id'])
646
+
647
+ if version:
648
+ version_obj = next((v for v in details['versions'] if v['name'] == version), None)
649
+ if not version_obj:
650
+ raise click.ClickException(f"Version '{version}' not found")
651
+ version_id = version_obj['id']
652
+ else:
653
+ # Use first version
654
+ version_id = details['versions'][0]['id']
655
+
656
+ # Display configuration
657
+ console.print()
658
+ console.print("✓ [green]Connected to platform agent[/green]")
659
+ console.print()
660
+
661
+ agent_executor = client.application(
662
+ application_id=agent['id'],
663
+ application_version_id=version_id,
664
+ memory=memory,
665
+ chat_history=chat_history
666
+ )
667
+ llm = None # Platform agents don't use direct LLM
668
+
669
+ # Interactive chat loop
670
+ while True:
671
+ try:
672
+ # Styled prompt
673
+ console.print("\n[bold bright_white]>[/bold bright_white] ", end="")
674
+ user_input = input().strip()
675
+
676
+ if not user_input:
677
+ continue
678
+
679
+ # Handle commands
680
+ if user_input.lower() in ['exit', 'quit']:
681
+ console.print("\n[bold cyan]👋 Goodbye![/bold cyan]\n")
682
+ break
683
+
684
+ if user_input == '/clear':
685
+ chat_history = []
686
+ console.print("[green]✓ Conversation history cleared.[/green]")
687
+ continue
688
+
689
+ if user_input == '/history':
690
+ if not chat_history:
691
+ console.print("[yellow]No conversation history yet.[/yellow]")
692
+ else:
693
+ console.print("\n[bold cyan]── Conversation History ──[/bold cyan]")
694
+ for i, msg in enumerate(chat_history, 1):
695
+ role = msg.get('role', 'unknown')
696
+ content = msg.get('content', '')
697
+ role_color = 'blue' if role == 'user' else 'green'
698
+ console.print(f"\n[bold {role_color}]{i}. {role.upper()}:[/bold {role_color}] {content[:100]}...")
699
+ continue
700
+
701
+ if user_input == '/save':
702
+ console.print("[yellow]Save to file (default: conversation.json):[/yellow] ", end="")
703
+ filename = input().strip()
704
+ filename = filename or "conversation.json"
705
+ with open(filename, 'w') as f:
706
+ json.dump({'history': chat_history}, f, indent=2)
707
+ console.print(f"[green]✓ Conversation saved to {filename}[/green]")
708
+ continue
709
+
710
+ if user_input == '/help':
711
+ _print_help()
712
+ continue
713
+
714
+ # Execute agent
715
+ if is_local:
716
+ # Local agent: use direct LLM call with streaming
717
+ messages = []
718
+ if system_prompt:
719
+ messages.append({"role": "system", "content": system_prompt})
720
+
721
+ # Add chat history
722
+ for msg in chat_history:
723
+ messages.append(msg)
724
+
725
+ # Add user message
726
+ messages.append({"role": "user", "content": user_input})
727
+
728
+ try:
729
+ # Try streaming if available
730
+ if hasattr(llm, 'stream'):
731
+ output_chunks = []
732
+ first_chunk = True
733
+
734
+ # Show spinner until first token arrives
735
+ status = console.status("[yellow]Thinking...[/yellow]", spinner="dots")
736
+ status.start()
737
+
738
+ # Stream the response token by token
739
+ for chunk in llm.stream(messages):
740
+ if hasattr(chunk, 'content'):
741
+ token = chunk.content
742
+ else:
743
+ token = str(chunk)
744
+
745
+ if token:
746
+ # Stop spinner and show agent name on first token
747
+ if first_chunk:
748
+ status.stop()
749
+ console.print(f"\n[bold bright_cyan]{agent_name}:[/bold bright_cyan]\n", end="")
750
+ first_chunk = False
751
+
752
+ console.print(token, end="", markup=False)
753
+ output_chunks.append(token)
754
+
755
+ # Stop status if still running (no tokens received)
756
+ if first_chunk:
757
+ status.stop()
758
+ console.print(f"\n[bold bright_cyan]{agent_name}:[/bold bright_cyan]\n", end="")
759
+
760
+ output = ''.join(output_chunks)
761
+ console.print() # New line after streaming
762
+ else:
763
+ # Fallback to non-streaming with spinner
764
+ with console.status("[yellow]Thinking...[/yellow]", spinner="dots"):
765
+ response = llm.invoke(messages)
766
+ if hasattr(response, 'content'):
767
+ output = response.content
768
+ else:
769
+ output = str(response)
770
+
771
+ # Display response after spinner stops
772
+ console.print(f"\n[bold bright_cyan]{agent_name}:[/bold bright_cyan]")
773
+ if any(marker in output for marker in ['```', '**', '##', '- ', '* ']):
774
+ console.print(Markdown(output))
775
+ else:
776
+ console.print(output)
777
+ except Exception as e:
778
+ console.print(f"\n[red]✗ Error: {e}[/red]\n")
779
+ continue
780
+ else:
781
+ # Platform agent: use agent executor
782
+ with console.status("[yellow]Thinking...[/yellow]", spinner="dots"):
783
+ result = agent_executor.invoke({
784
+ "input": [user_input],
785
+ "chat_history": chat_history
786
+ })
787
+ output = result.get('output', '')
788
+
789
+ # Display response
790
+ console.print(f"\n[bold bright_cyan]{agent_name}:[/bold bright_cyan]")
791
+ if any(marker in output for marker in ['```', '**', '##', '- ', '* ']):
792
+ console.print(Markdown(output))
793
+ else:
794
+ console.print(output)
795
+
796
+ # Update chat history
797
+ chat_history.append({"role": "user", "content": user_input})
798
+ chat_history.append({"role": "assistant", "content": output})
799
+
800
+ except KeyboardInterrupt:
801
+ console.print("\n\n[yellow]Interrupted. Type 'exit' to quit or continue chatting.[/yellow]")
802
+ continue
803
+ except EOFError:
804
+ console.print("\n\n[bold cyan]Goodbye! 👋[/bold cyan]")
805
+ break
806
+
807
+ except click.ClickException:
808
+ raise
809
+ except Exception as e:
810
+ logger.exception("Failed to start chat")
811
+ error_panel = Panel(
812
+ str(e),
813
+ title="Error",
814
+ border_style="red",
815
+ box=box.ROUNDED
816
+ )
817
+ console.print(error_panel, style="red")
818
+ raise click.Abort()
819
+
820
+
821
+ @agent.command('run')
822
+ @click.argument('agent_source')
823
+ @click.argument('message')
824
+ @click.option('--version', help='Agent version (for platform agents)')
825
+ @click.option('--toolkit-config', multiple=True, type=click.Path(exists=True),
826
+ help='Toolkit configuration files')
827
+ @click.option('--model', help='Override LLM model')
828
+ @click.option('--temperature', type=float, help='Override temperature')
829
+ @click.option('--max-tokens', type=int, help='Override max tokens')
830
+ @click.option('--save-thread', help='Save thread ID to file for continuation')
831
+ @click.pass_context
832
+ def agent_run(ctx, agent_source: str, message: str, version: Optional[str],
833
+ toolkit_config: tuple, model: Optional[str],
834
+ temperature: Optional[float], max_tokens: Optional[int],
835
+ save_thread: Optional[str]):
836
+ """
837
+ Run agent with a single message (handoff mode).
838
+
839
+ AGENT_SOURCE can be:
840
+ - Platform agent ID or name
841
+ - Path to local agent file
842
+
843
+ MESSAGE is the input message to send to the agent.
844
+
845
+ Examples:
846
+
847
+ # Simple query
848
+ alita-cli agent run my-agent "What is the status of JIRA-123?"
849
+
850
+ # With local agent
851
+ alita-cli agent run .github/agents/sdk-dev.agent.md \\
852
+ "Create a new toolkit for Stripe API"
853
+
854
+ # With toolkit configs and JSON output
855
+ alita-cli --output json agent run my-agent "Search for bugs" \\
856
+ --toolkit-config jira-config.json
857
+
858
+ # Save thread for continuation
859
+ alita-cli agent run my-agent "Start task" \\
860
+ --save-thread thread.txt
861
+ """
862
+ formatter = ctx.obj['formatter']
863
+ client = get_client(ctx)
864
+
865
+ try:
866
+ # Load agent
867
+ is_local = Path(agent_source).exists()
868
+
869
+ # Load toolkits
870
+ toolkit_configs = []
871
+
872
+ if is_local:
873
+ agent_def = load_agent_definition(agent_source)
874
+ agent_name = agent_def.get('name', Path(agent_source).stem)
875
+
876
+ # Load toolkit configs from agent definition
877
+ if 'toolkit_configs' in agent_def:
878
+ for tk_config in agent_def['toolkit_configs']:
879
+ if isinstance(tk_config, dict):
880
+ if 'file' in tk_config:
881
+ config = load_toolkit_config(tk_config['file'])
882
+ toolkit_configs.append(config)
883
+ elif 'config' in tk_config:
884
+ toolkit_configs.append(tk_config['config'])
885
+
886
+ # Load additional toolkit configs from --toolkit-config options
887
+ if toolkit_config:
888
+ for config_path in toolkit_config:
889
+ config = load_toolkit_config(config_path)
890
+ toolkit_configs.append(config)
891
+
892
+ # Get LLM configuration
893
+ llm_model = model or agent_def.get('model', 'gpt-4o')
894
+ llm_temperature = temperature if temperature is not None else agent_def.get('temperature', 0.7)
895
+ llm_max_tokens = max_tokens or agent_def.get('max_tokens', 2000)
896
+ system_prompt = agent_def.get('system_prompt', '')
897
+
898
+ # Create LLM instance
899
+ try:
900
+ llm = client.get_llm(
901
+ model_name=llm_model,
902
+ model_config={
903
+ 'temperature': llm_temperature,
904
+ 'max_tokens': llm_max_tokens
905
+ }
906
+ )
907
+ except Exception as e:
908
+ error_panel = Panel(
909
+ f"Failed to create LLM instance: {e}",
910
+ title="Error",
911
+ border_style="red",
912
+ box=box.ROUNDED
913
+ )
914
+ console.print(error_panel, style="red")
915
+ raise click.Abort()
916
+
917
+ # Prepare messages
918
+ messages = []
919
+ if system_prompt:
920
+ messages.append({"role": "system", "content": system_prompt})
921
+ messages.append({"role": "user", "content": message})
922
+
923
+ # Execute with spinner for non-JSON output
924
+ if formatter.__class__.__name__ == 'JSONFormatter':
925
+ response = llm.invoke(messages)
926
+ if hasattr(response, 'content'):
927
+ output = response.content
928
+ else:
929
+ output = str(response)
930
+
931
+ click.echo(formatter._dump({
932
+ 'agent': agent_name,
933
+ 'message': message,
934
+ 'response': output
935
+ }))
936
+ else:
937
+ # Show spinner while executing
938
+ with console.status("[yellow]Processing...[/yellow]", spinner="dots"):
939
+ response = llm.invoke(messages)
940
+ if hasattr(response, 'content'):
941
+ output = response.content
942
+ else:
943
+ output = str(response)
944
+
945
+ # Format and display output
946
+ console.print(f"\n[bold cyan]🤖 Agent: {agent_name}[/bold cyan]\n")
947
+ console.print(f"[bold]Message:[/bold] {message}\n")
948
+ console.print("[bold]Response:[/bold]")
949
+ # Render markdown if the response looks like it contains markdown
950
+ if any(marker in output for marker in ['```', '**', '##', '- ', '* ']):
951
+ console.print(Markdown(output))
952
+ else:
953
+ console.print(output)
954
+ console.print()
955
+
956
+ else:
957
+ # Platform agent
958
+ agents = client.get_list_of_apps()
959
+ agent = None
960
+
961
+ try:
962
+ agent_id = int(agent_source)
963
+ agent = next((a for a in agents if a['id'] == agent_id), None)
964
+ except ValueError:
965
+ agent = next((a for a in agents if a['name'] == agent_source), None)
966
+
967
+ if not agent:
968
+ raise click.ClickException(f"Agent '{agent_source}' not found")
969
+
970
+ # Get version
971
+ details = client.get_app_details(agent['id'])
972
+
973
+ if version:
974
+ version_obj = next((v for v in details['versions'] if v['name'] == version), None)
975
+ if not version_obj:
976
+ raise click.ClickException(f"Version '{version}' not found")
977
+ version_id = version_obj['id']
978
+ else:
979
+ version_id = details['versions'][0]['id']
980
+
981
+ # Load additional toolkit configs from --toolkit-config options
982
+ if toolkit_config:
983
+ for config_path in toolkit_config:
984
+ config = load_toolkit_config(config_path)
985
+ toolkit_configs.append(config)
986
+
987
+ # Create memory
988
+ from langgraph.checkpoint.sqlite import SqliteSaver
989
+ memory = SqliteSaver(sqlite3.connect(":memory:", check_same_thread=False))
990
+
991
+ # Create agent executor
992
+ agent_executor = client.application(
993
+ application_id=agent['id'],
994
+ application_version_id=version_id,
995
+ memory=memory
996
+ )
997
+
998
+ # Execute with spinner for non-JSON output
999
+ if formatter.__class__.__name__ == 'JSONFormatter':
1000
+ result = agent_executor.invoke({
1001
+ "input": [message],
1002
+ "chat_history": []
1003
+ })
1004
+
1005
+ click.echo(formatter._dump({
1006
+ 'agent': agent['name'],
1007
+ 'message': message,
1008
+ 'response': result.get('output', ''),
1009
+ 'full_result': result
1010
+ }))
1011
+ else:
1012
+ # Show spinner while executing
1013
+ with console.status("[yellow]Processing...[/yellow]", spinner="dots"):
1014
+ result = agent_executor.invoke({
1015
+ "input": [message],
1016
+ "chat_history": []
1017
+ })
1018
+
1019
+ # Format and display output
1020
+ console.print(f"\n[bold cyan]🤖 Agent: {agent['name']}[/bold cyan]\n")
1021
+ console.print(f"[bold]Message:[/bold] {message}\n")
1022
+ console.print("[bold]Response:[/bold]")
1023
+ response = result.get('output', 'No response')
1024
+ # Render markdown if the response looks like it contains markdown
1025
+ if any(marker in response for marker in ['```', '**', '##', '- ', '* ']):
1026
+ console.print(Markdown(response))
1027
+ else:
1028
+ console.print(response)
1029
+ console.print()
1030
+
1031
+ # Save thread if requested
1032
+ if save_thread:
1033
+ thread_data = {
1034
+ 'agent_id': agent['id'],
1035
+ 'agent_name': agent['name'],
1036
+ 'version_id': version_id,
1037
+ 'thread_id': result.get('thread_id'),
1038
+ 'last_message': message
1039
+ }
1040
+ with open(save_thread, 'w') as f:
1041
+ json.dump(thread_data, f, indent=2)
1042
+ logger.info(f"Thread saved to {save_thread}")
1043
+
1044
+ except click.ClickException:
1045
+ raise
1046
+ except Exception as e:
1047
+ logger.exception("Failed to run agent")
1048
+ error_panel = Panel(
1049
+ str(e),
1050
+ title="Error",
1051
+ border_style="red",
1052
+ box=box.ROUNDED
1053
+ )
1054
+ console.print(error_panel, style="red")
1055
+ raise click.Abort()