Add dual-prefix AI command

This commit is contained in:
Codex
2026-05-14 13:24:47 +02:00
parent 27b2760079
commit 2a731600e0
16 changed files with 607 additions and 16 deletions

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from textwrap import wrap
from typing import TYPE_CHECKING, Any
from app.ai import AIInvocation, parse_ai_invocation
from app.commands.core import CommandRegistry, normalize_command_name
if TYPE_CHECKING:
@@ -31,6 +33,58 @@ def _display_command(command: str) -> str:
return f"..{command}"
def _chunk_text(text: str, limit: int = 3900) -> list[str]:
if len(text) <= limit:
return [text]
chunks: list[str] = []
current: list[str] = []
current_len = 0
for paragraph in text.split("\n"):
paragraph = paragraph.rstrip()
piece = paragraph if not current else "\n" + paragraph
if current_len + len(piece) <= limit:
current.append(piece.lstrip("\n") if not current else piece)
current_len += len(piece)
continue
if current:
chunks.append("".join(current))
current = []
current_len = 0
if len(paragraph) <= limit:
current = [paragraph]
current_len = len(paragraph)
continue
for line in wrap(paragraph, width=limit):
if len(line) <= limit:
chunks.append(line)
else:
chunks.extend([line[i : i + limit] for i in range(0, len(line), limit)])
if current:
chunks.append("".join(current))
return [chunk for chunk in chunks if chunk]
async def _reply_long(ctx: "CommandContext", text: str) -> None:
chunks = _chunk_text(text)
if not chunks:
await ctx.reply("")
return
first = await ctx.reply(chunks[0])
for chunk in chunks[1:]:
await ctx.client.send_message(
ctx.message.chat.id,
chunk,
reply_to_message_id=getattr(first, "id", None),
)
def _format_ai_help(ctx: "CommandContext") -> str:
ai_service = ctx.manager.ai_service
if ai_service is None:
return ""
return "AI models: " + ai_service.describe_models()
def _format_permissions(ctx: "CommandContext") -> str:
store = ctx.acl_store
target = ctx.replied_user
@@ -163,6 +217,9 @@ async def status(ctx: "CommandContext") -> None:
f"allow_rules={stats['allow_rules']}, "
f"deny_rules={stats['deny_rules']}"
)
ai_service = ctx.manager.ai_service
if ai_service is not None:
lines.append("AI: " + ai_service.describe_models())
await ctx.reply("\n".join(lines))
@@ -173,7 +230,12 @@ async def status(ctx: "CommandContext") -> None:
category="system",
)
async def help_command(ctx: "CommandContext") -> None:
await ctx.reply(ctx.manager.command_registry.render_help(ctx))
lines = [ctx.manager.command_registry.render_help(ctx)]
ai_help = _format_ai_help(ctx)
if ai_help:
lines.append("")
lines.append(ai_help)
await ctx.reply("\n".join(lines))
@command_registry.command(
@@ -209,12 +271,48 @@ async def deny(ctx: "CommandContext") -> None:
@command_registry.command(
"echo",
"Echo text for granted users",
usage="..echo <text>",
usage=".echo / ..echo <text>",
category="shared",
).shared()
async def echo(ctx: "CommandContext") -> None:
text = ctx.args.strip()
if not text:
await ctx.reply("Usage: ..echo <text>")
await ctx.reply("Usage: .echo / ..echo <text>")
return
await ctx.reply(text)
@command_registry.command(
"ai",
"Ask a configurable AI model",
usage=".ai / ..ai [N] [--model] prompt",
category="ai",
).shared()
async def ai(ctx: "CommandContext") -> None:
ai_service = ctx.manager.ai_service
if ai_service is None or ctx.manager.config.ai is None:
await ctx.reply("AI is not configured")
return
try:
invocation = parse_ai_invocation(
ctx.args,
default_context_messages=ctx.manager.config.ai.context_messages,
)
selected_alias = ai_service.select_model_alias(invocation.model_flags)
response = await ai_service.generate(
ctx,
AIInvocation(
context_messages=invocation.context_messages,
model_flags=[selected_alias],
prompt=invocation.prompt,
),
)
except ValueError as exc:
await ctx.reply(str(exc))
return
except RuntimeError as exc:
await ctx.reply(str(exc))
return
await _reply_long(ctx, response.text)