Skip to content

Commit 0e87900

Browse files
authored
Merge pull request #637 from lbedner/v0.6.11-rc3
v0.6.11-rc3
2 parents e35da92 + 74b8f7a commit 0e87900

54 files changed

Lines changed: 825 additions & 588 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Each generated project includes:
3232

3333
## Installation
3434

35-
**Current Version**: 0.6.11rc2
35+
**Current Version**: 0.6.11rc3
3636

3737
```bash
3838
pip install aegis-stack
@@ -67,6 +67,34 @@ This project uses `uv` for dependency management and a `Makefile` for CLI develo
6767

6868
**Template testing is critical** - always run `make test-template` after modifying templates to ensure generated projects work correctly.
6969

70+
### Pre-RC verification (REQUIRED before any version bump)
71+
72+
Before suggesting a version/rc bump, run BOTH locally:
73+
74+
1. **`make check`** — the aegis-stack repo's OWN test suite (lint +
75+
typecheck + pytest). Catches bugs in the CLI / core code itself,
76+
including config tests that gate on constants like
77+
``DEFAULT_PYTHON_VERSION``. Without this, changing a constant
78+
without updating its assertion test passes locally but fails CI.
79+
80+
2. **`make test-stacks-full`** — the matrix that runs each generated
81+
stack through generation → install → lint → typecheck → pytest.
82+
Catches drift in templates / cross-stack issues.
83+
84+
History shows multiple rcs in a row were "green locally" by `aegis init` +
85+
`make check` (on the GENERATED project) on a single stack, but failed CI
86+
because either:
87+
- The matrix was never run (caught template drift on stacks the local
88+
test didn't generate), or
89+
- The aegis-stack repo's own tests were never run (caught constant /
90+
config changes without test updates).
91+
92+
Running both targets is the only way to match what CI actually runs.
93+
94+
If the matrix takes too long during iteration, use `make test-stacks-quick`
95+
for fast feedback (3 representative stacks: base, everything, insights),
96+
then run the full matrix once at the end.
97+
7098
### TestPyPI Release Testing
7199
Test upgrade paths using TestPyPI before publishing to PyPI:
72100

aegis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Aegis Stack CLI - Component generation and project management tools.
33
"""
44

5-
__version__ = "0.6.11rc2"
5+
__version__ = "0.6.11rc3"

aegis/config/defaults.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,16 @@ def _generate_supported_versions(min_version: str, max_version: str) -> list[str
8888
# Parse bounds from pyproject.toml (single source of truth)
8989
_min_version, _max_version = _parse_python_version_bounds()
9090

91-
# Default Python version for generated projects (maximum supported)
92-
# Users can still specify --python-version 3.11 or 3.12 if desired
93-
DEFAULT_PYTHON_VERSION = _max_version
91+
# Default Python version for generated projects.
92+
#
93+
# Pinned to 3.13 (not the auto-derived max of 3.14) because the 3.14
94+
# ecosystem is still incomplete: openai 2.x has a circular import on
95+
# pytest collection under 3.14, and ``requests.compat`` is missing
96+
# ``JSONDecodeError``. Both break ``make check`` in matrix tests.
97+
# 3.13 has been out long enough that all our pinned deps work.
98+
# Users can still opt into 3.14 explicitly via ``--python-version 3.14``
99+
# (or 3.11 / 3.12 — anything in SUPPORTED_PYTHON_VERSIONS works).
100+
DEFAULT_PYTHON_VERSION = "3.13"
94101

95102
# Supported Python versions (auto-generated from min to max)
96103
SUPPORTED_PYTHON_VERSIONS = _generate_supported_versions(_min_version, _max_version)

aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ lint: ## Check code style with ruff
129129

130130
fix: ## Auto-fix linting and formatting issues
131131
@echo "Auto-fixing code issues..."
132-
@uv run ruff check . --fix
132+
@-uv run ruff check . --fix
133133
@uv run ruff format .
134134

135135
format: ## Format code with ruff

aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja

Lines changed: 108 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ def use_provider(
391391
existing_key = get_existing_api_key(provider)
392392

393393
if not existing_key:
394-
console.print(f" [yellow]![/yellow] {t('ai.no_api_key_configured', var=env_var_name)}")
394+
warning = t("ai.no_api_key_configured", var=env_var_name)
395+
console.print(f" [yellow]![/yellow] {warning}")
395396
console.print()
396397
console.print(f"[yellow]{t('ai.consider_running')}[/yellow]")
397398
console.print(
@@ -526,8 +527,8 @@ def chat(
526527
typer.echo("\nChat interrupted", err=True)
527528
raise typer.Exit(1)
528529
except Exception as e:
529-
error_str = str(e)
530530
{% if ai_rag %}
531+
error_str = str(e)
531532
# Provide helpful guidance for RAG collection errors
532533
if "No RAG collection specified" in error_str:
533534
console.print()
@@ -649,7 +650,9 @@ def voice(
649650
# Validate file exists
650651
audio_path = Path(audio_file)
651652
if not audio_path.exists():
652-
console.print(f"[red]{t('shared.error')}[/red] {t('ai.file_not_found', path=audio_file)}")
653+
err_label = t("shared.error")
654+
detail = t("ai.file_not_found", path=audio_file)
655+
console.print(f"[red]{err_label}[/red] {detail}")
653656
raise typer.Exit(1)
654657

655658
# Determine audio format
@@ -658,7 +661,9 @@ def voice(
658661
audio_format = AudioFormat(ext)
659662
except ValueError:
660663
supported = ", ".join(f.value for f in AudioFormat)
661-
console.print(f"[red]{t('shared.error')}[/red] {t('ai.unsupported_format', ext=ext)}")
664+
err_label = t("shared.error")
665+
detail = t("ai.unsupported_format", ext=ext)
666+
console.print(f"[red]{err_label}[/red] {detail}")
662667
console.print(f"[dim]{t('ai.supported_formats', formats=supported)}[/dim]")
663668
raise typer.Exit(1)
664669

@@ -704,11 +709,19 @@ def voice(
704709
console.print(f" {result.transcription.text}")
705710

706711
if result.transcription.language:
707-
console.print(f" [dim]{t('ai.language_label', lang=result.transcription.language)}[/dim]")
712+
lang_label = t(
713+
"ai.language_label",
714+
lang=result.transcription.language,
715+
)
716+
console.print(f" [dim]{lang_label}[/dim]")
708717
if result.transcription.duration_seconds:
709-
console.print(
710-
f" [dim]{t('ai.speech_duration', duration=f'{result.transcription.duration_seconds:.1f}')}[/dim]"
718+
duration_str = (
719+
f"{result.transcription.duration_seconds:.1f}"
720+
)
721+
duration_label = t(
722+
"ai.speech_duration", duration=duration_str
711723
)
724+
console.print(f" [dim]{duration_label}[/dim]")
712725

713726
console.print()
714727
console.print(f"[bold green]{t('ai.response_label')}[/bold green]")
@@ -725,9 +738,11 @@ def voice(
725738

726739
if result.conversation_id:
727740
console.print()
728-
console.print(
729-
f"[dim]{t('ai.conversation_id_label', id=result.conversation_id[:8])}...[/dim]"
741+
conv_label = t(
742+
"ai.conversation_id_label",
743+
id=result.conversation_id[:8],
730744
)
745+
console.print(f"[dim]{conv_label}...[/dim]")
731746

732747
except KeyboardInterrupt:
733748
typer.echo(f"\n{t('ai.cancelled')}", err=True)
@@ -794,7 +809,12 @@ def record(
794809
sample_rate = int(device_info['default_samplerate'])
795810
channels = 1 # Mono
796811

797-
console.print(f"[dim]{t('ai.using_mic', name=device_info['name'], rate=sample_rate)}[/dim]")
812+
mic_label = t(
813+
"ai.using_mic",
814+
name=device_info["name"],
815+
rate=sample_rate,
816+
)
817+
console.print(f"[dim]{mic_label}[/dim]")
798818

799819
# Determine output path
800820
if output:
@@ -850,8 +870,11 @@ def record(
850870
minutes = int(elapsed // 60)
851871
seconds = int(elapsed % 60)
852872
# Use regular print with \r for proper carriage return
873+
timer_str = (
874+
f"{minutes:02d}:{seconds:02d}"
875+
)
853876
print(
854-
f"\r \033[91m●\033[0m Recording: {minutes:02d}:{seconds:02d}",
877+
f"\r \033[91m●\033[0m Recording: {timer_str}",
855878
end="",
856879
flush=True,
857880
)
@@ -880,7 +903,13 @@ def record(
880903
# Save to WAV file
881904
sf.write(str(audio_path), audio_data, sample_rate)
882905

883-
console.print(f"[dim]{t('ai.audio_saved', path=str(audio_path), size=f'{audio_path.stat().st_size:,}')}[/dim]")
906+
size_str = f"{audio_path.stat().st_size:,}"
907+
saved_label = t(
908+
"ai.audio_saved",
909+
path=str(audio_path),
910+
size=size_str,
911+
)
912+
console.print(f"[dim]{saved_label}[/dim]")
884913

885914
# Read audio file
886915
with open(audio_path, "rb") as f:
@@ -949,9 +978,11 @@ def record(
949978

950979
if chat_result.metadata.get("conversation_id"):
951980
console.print()
952-
console.print(
953-
f"[dim]{t('ai.conversation_id_label', id=chat_result.metadata['conversation_id'][:8])}...[/dim]"
981+
conv_label = t(
982+
"ai.conversation_id_label",
983+
id=chat_result.metadata["conversation_id"][:8],
954984
)
985+
console.print(f"[dim]{conv_label}...[/dim]")
955986

956987
# Play TTS response if requested
957988
if voice_response:
@@ -1047,7 +1078,9 @@ def transcribe(
10471078
# Validate file exists
10481079
audio_path = Path(audio_file)
10491080
if not audio_path.exists():
1050-
console.print(f"[red]{t('shared.error')}[/red] {t('ai.file_not_found', path=audio_file)}")
1081+
err_label = t("shared.error")
1082+
detail = t("ai.file_not_found", path=audio_file)
1083+
console.print(f"[red]{err_label}[/red] {detail}")
10511084
raise typer.Exit(1)
10521085

10531086
# Determine audio format
@@ -1056,7 +1089,9 @@ def transcribe(
10561089
audio_format = AudioFormat(ext)
10571090
except ValueError:
10581091
supported = ", ".join(f.value for f in AudioFormat)
1059-
console.print(f"[red]{t('shared.error')}[/red] {t('ai.unsupported_format', ext=ext)}")
1092+
err_label = t("shared.error")
1093+
detail = t("ai.unsupported_format", ext=ext)
1094+
console.print(f"[red]{err_label}[/red] {detail}")
10601095
console.print(f"[dim]{t('ai.supported_formats', formats=supported)}[/dim]")
10611096
raise typer.Exit(1)
10621097

@@ -1208,7 +1243,11 @@ def speak(
12081243
console.print(f" [dim]{t('ai.speech_size', size=f'{len(result.audio):,}')}[/dim]")
12091244

12101245
if result.duration_seconds:
1211-
console.print(f" [dim]{t('ai.speech_duration', duration=f'{result.duration_seconds:.1f}')}[/dim]")
1246+
duration_str = f"{result.duration_seconds:.1f}"
1247+
duration_label = t(
1248+
"ai.speech_duration", duration=duration_str
1249+
)
1250+
console.print(f" [dim]{duration_label}[/dim]")
12121251

12131252
except KeyboardInterrupt:
12141253
typer.echo(f"\n{t('ai.cancelled')}", err=True)
@@ -1362,12 +1401,20 @@ def usage(
13621401
# Display functions
13631402
def display_summary_panel(stats: UsageStatsResponse) -> None:
13641403
success_color = get_success_color(stats.success_rate)
1404+
tokens_label = t("ai.usage_total_tokens")
1405+
cost_label = t("ai.usage_total_cost")
1406+
requests_label = t("ai.usage_total_requests")
1407+
rate_label = t("ai.usage_success_rate")
1408+
tokens_str = format_number(stats.total_tokens)
1409+
cost_str = format_cost(stats.total_cost)
1410+
requests_str = format_number(stats.total_requests)
1411+
rate_str = format_percentage(stats.success_rate)
13651412
summary_lines = [
1366-
f"[bold cyan]{t('ai.usage_total_tokens')}[/bold cyan] {format_number(stats.total_tokens)}",
1367-
f"[bold cyan]{t('ai.usage_total_cost')}[/bold cyan] {format_cost(stats.total_cost)}",
1368-
f"[bold cyan]{t('ai.usage_total_requests')}[/bold cyan] {format_number(stats.total_requests)}",
1369-
f"[bold cyan]{t('ai.usage_success_rate')}[/bold cyan] "
1370-
f"[{success_color}]{format_percentage(stats.success_rate)}[/{success_color}]",
1413+
f"[bold cyan]{tokens_label}[/bold cyan] {tokens_str}",
1414+
f"[bold cyan]{cost_label}[/bold cyan] {cost_str}",
1415+
f"[bold cyan]{requests_label}[/bold cyan] {requests_str}",
1416+
f"[bold cyan]{rate_label}[/bold cyan] "
1417+
f"[{success_color}]{rate_str}[/{success_color}]",
13711418
]
13721419
console.print(
13731420
Panel(
@@ -1388,8 +1435,14 @@ def usage(
13881435
console.print(f"\n[bold blue]{t('ai.token_breakdown')}[/bold blue]")
13891436
input_str = format_number(stats.input_tokens)
13901437
output_str = format_number(stats.output_tokens)
1391-
console.print(f" [cyan]{t('ai.input_tokens')}[/cyan] {input_str:>12} ({input_pct:.0f}%)")
1392-
console.print(f" [cyan]{t('ai.output_tokens')}[/cyan] {output_str:>12} ({output_pct:.0f}%)")
1438+
in_label = t("ai.input_tokens")
1439+
out_label = t("ai.output_tokens")
1440+
console.print(
1441+
f" [cyan]{in_label}[/cyan] {input_str:>12} ({input_pct:.0f}%)"
1442+
)
1443+
console.print(
1444+
f" [cyan]{out_label}[/cyan] {output_str:>12} ({output_pct:.0f}%)"
1445+
)
13931446
bar_width = 40
13941447
input_bars = int((input_pct / 100) * bar_width)
13951448
output_bars = bar_width - input_bars
@@ -1646,15 +1699,24 @@ async def _interactive_chat_session(
16461699
ai_config = get_ai_config(settings)
16471700

16481701
console.print()
1649-
console.print(f"[bold bright_magenta]Illiana[/bold bright_magenta] [dim]v{__aegis_version__}[/dim]")
1702+
banner = (
1703+
f"[bold bright_magenta]Illiana[/bold bright_magenta] "
1704+
f"[dim]v{__aegis_version__}[/dim]"
1705+
)
1706+
console.print(banner)
16501707
console.print()
16511708

16521709
# Boot steps with brief delays for effect
16531710
console.print(f" [dim]>[/dim] {t('ai.initializing')}", end="")
16541711
await asyncio.sleep(0.15)
16551712
console.print(f" [green]{t('ai.ok')}[/green]")
16561713

1657-
console.print(f" [dim]>[/dim] {t('ai.connecting_to', provider=get_provider_display_name(ai_config.provider))}", end="")
1714+
provider_name = get_provider_display_name(ai_config.provider)
1715+
connecting_label = t("ai.connecting_to", provider=provider_name)
1716+
console.print(
1717+
f" [dim]>[/dim] {connecting_label}",
1718+
end="",
1719+
)
16581720
# Warm up the agent (lazy imports, model initialization)
16591721
from app.services.ai.providers import get_agent
16601722
_ = get_agent(ai_config, settings)
@@ -1694,7 +1756,12 @@ async def _interactive_chat_session(
16941756
console.print(f"[green]{t('ai.health_ok', pct=health_pct)}[/green]")
16951757
else:
16961758
unhealthy_count = len(status.unhealthy_components)
1697-
console.print(f"[yellow]{t('ai.health_degraded', pct=health_pct, count=unhealthy_count)}[/yellow]")
1759+
degraded_label = t(
1760+
"ai.health_degraded",
1761+
pct=health_pct,
1762+
count=unhealthy_count,
1763+
)
1764+
console.print(f"[yellow]{degraded_label}[/yellow]")
16981765
except Exception:
16991766
console.print(f"[dim]{t('ai.health_na')}[/dim]")
17001767

@@ -1948,9 +2015,15 @@ async def _interactive_chat_session(
19482015
except ProviderNotInstalledError as e:
19492016
# Clean display for missing provider
19502017
console.print()
1951-
console.print(f"[yellow]{t('ai.provider_not_installed', provider=e.provider)}[/yellow]")
2018+
missing_label = t(
2019+
"ai.provider_not_installed", provider=e.provider
2020+
)
2021+
console.print(f"[yellow]{missing_label}[/yellow]")
19522022
console.print()
1953-
console.print(f"[green]{t('ai.run_to_install', command=e.cli_command)}[/green]")
2023+
install_label = t(
2024+
"ai.run_to_install", command=e.cli_command
2025+
)
2026+
console.print(f"[green]{install_label}[/green]")
19542027
console.print()
19552028
except Exception as stream_error:
19562029
console.print(f"[red]{t('shared.error')} {stream_error}[/red]")
@@ -2156,9 +2229,13 @@ async def _stream_chat_response(
21562229
except ProviderNotInstalledError as e:
21572230
# Clean display for missing provider - no need to re-raise
21582231
console.print()
2159-
console.print(f"[yellow]{t('ai.provider_not_installed', provider=e.provider)}[/yellow]")
2232+
missing_label = t(
2233+
"ai.provider_not_installed", provider=e.provider
2234+
)
2235+
console.print(f"[yellow]{missing_label}[/yellow]")
21602236
console.print()
2161-
console.print(f"[green]{t('ai.run_to_install', command=e.cli_command)}[/green]")
2237+
install_label = t("ai.run_to_install", command=e.cli_command)
2238+
console.print(f"[green]{install_label}[/green]")
21622239
console.print()
21632240
return None
21642241

0 commit comments

Comments
 (0)