mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 14:14:57 +08:00
core/cli: provider selection, event display, REPL state
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from plyngent.agent import (
|
||||
MaxRoundsEvent,
|
||||
TextDeltaEvent,
|
||||
ToolCallEvent,
|
||||
ToolResultEvent,
|
||||
)
|
||||
from plyngent.lmproto.openai_compatible.model import AssistantFunctionToolCall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from plyngent.agent import AgentEvent
|
||||
|
||||
_TOOL_RESULT_PREVIEW = 200
|
||||
|
||||
|
||||
async def render_events(events: AsyncIterator[AgentEvent]) -> None:
|
||||
"""Print agent events to the terminal."""
|
||||
printed_text = False
|
||||
async for event in events:
|
||||
if isinstance(event, TextDeltaEvent):
|
||||
if not printed_text:
|
||||
click.echo()
|
||||
click.secho("assistant: ", fg="cyan", nl=False)
|
||||
printed_text = True
|
||||
click.echo(event.content, nl=False)
|
||||
elif isinstance(event, ToolCallEvent):
|
||||
call = event.tool_call
|
||||
if isinstance(call, AssistantFunctionToolCall):
|
||||
click.secho(
|
||||
f"\n[tool call] {call.function.name}({call.function.arguments})",
|
||||
fg="yellow",
|
||||
)
|
||||
else:
|
||||
click.secho(f"\n[tool call] custom id={call.id}", fg="yellow")
|
||||
elif isinstance(event, ToolResultEvent):
|
||||
content = event.message.content
|
||||
preview = (
|
||||
content
|
||||
if len(content) <= _TOOL_RESULT_PREVIEW
|
||||
else content[:_TOOL_RESULT_PREVIEW] + "…"
|
||||
)
|
||||
click.secho(f"[tool result] {preview}", fg="magenta")
|
||||
elif isinstance(event, MaxRoundsEvent):
|
||||
click.secho(f"\n[max rounds reached: {event.rounds}]", fg="red")
|
||||
else:
|
||||
# AssistantMessageEvent — text already shown via TextDeltaEvent.
|
||||
_ = event
|
||||
if printed_text:
|
||||
click.echo()
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from plyngent.config.models import Provider
|
||||
|
||||
|
||||
def select_provider(
|
||||
providers: Mapping[str, Provider],
|
||||
*,
|
||||
preferred: str | None = None,
|
||||
) -> tuple[str, Provider]:
|
||||
"""Pick a provider by name or interactive prompt."""
|
||||
if not providers:
|
||||
msg = "no providers configured; edit your plyngent.toml"
|
||||
raise click.ClickException(msg)
|
||||
|
||||
names = sorted(providers.keys())
|
||||
if preferred is not None:
|
||||
if preferred not in providers:
|
||||
msg = f"unknown provider {preferred!r}; available: {', '.join(names)}"
|
||||
raise click.ClickException(msg)
|
||||
return preferred, providers[preferred]
|
||||
|
||||
if len(names) == 1:
|
||||
name = names[0]
|
||||
click.echo(f"Using provider: {name}")
|
||||
return name, providers[name]
|
||||
|
||||
click.echo("Available providers:")
|
||||
for index, name in enumerate(names, start=1):
|
||||
preset = type(providers[name]).__struct_config__.tag
|
||||
click.echo(f" {index}. {name} ({preset})")
|
||||
choice = click.prompt("Select provider", type=click.Choice(names), show_choices=True)
|
||||
return choice, providers[choice]
|
||||
|
||||
|
||||
def select_model(
|
||||
provider: Provider,
|
||||
*,
|
||||
preferred: str | None = None,
|
||||
) -> str:
|
||||
"""Pick a model id from provider.models or free-form prompt."""
|
||||
model_names = sorted(provider.models.keys())
|
||||
if preferred is not None:
|
||||
if model_names and preferred not in provider.models:
|
||||
msg = f"unknown model {preferred!r}; available: {', '.join(model_names)}"
|
||||
raise click.ClickException(msg)
|
||||
return preferred
|
||||
|
||||
if len(model_names) == 1:
|
||||
model = model_names[0]
|
||||
click.echo(f"Using model: {model}")
|
||||
return model
|
||||
|
||||
if model_names:
|
||||
click.echo("Available models:")
|
||||
for index, name in enumerate(model_names, start=1):
|
||||
click.echo(f" {index}. {name}")
|
||||
return click.prompt("Select model", type=click.Choice(model_names), show_choices=True)
|
||||
|
||||
return click.prompt("Model id (not listed in config)", type=str)
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from plyngent.agent import ChatAgent, ChatClient, ToolRegistry
|
||||
from plyngent.runtime import create_client
|
||||
from plyngent.tools import DEFAULT_TOOLS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from plyngent.config.models import Provider
|
||||
from plyngent.config.store import ConfigStore
|
||||
from plyngent.memory import MemoryStore
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReplState:
|
||||
"""Mutable REPL session state."""
|
||||
|
||||
config: ConfigStore
|
||||
memory: MemoryStore
|
||||
workspace: Path
|
||||
provider_name: str
|
||||
provider: Provider
|
||||
model: str
|
||||
tools_enabled: bool
|
||||
client: ChatClient = field(init=False)
|
||||
agent: ChatAgent = field(init=False)
|
||||
session_id: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# DeepSeek client uses a compatible but distinct param type; treat as ChatClient.
|
||||
self.client = cast("ChatClient", create_client(self.provider))
|
||||
self.agent = self._make_agent()
|
||||
|
||||
def _tool_registry(self) -> ToolRegistry | None:
|
||||
if not self.tools_enabled:
|
||||
return None
|
||||
return ToolRegistry(list(DEFAULT_TOOLS))
|
||||
|
||||
def _make_agent(self) -> ChatAgent:
|
||||
return ChatAgent(
|
||||
self.client,
|
||||
model=self.model,
|
||||
tools=self._tool_registry(),
|
||||
memory=self.memory,
|
||||
session_id=self.session_id,
|
||||
)
|
||||
|
||||
def rebuild_client(self) -> None:
|
||||
"""Recreate client and agent after provider/model/tools change."""
|
||||
messages = list(self.agent.messages)
|
||||
self.client = cast("ChatClient", create_client(self.provider))
|
||||
self.agent = self._make_agent()
|
||||
self.agent.messages = messages
|
||||
|
||||
async def new_session(self, name: str = "chat") -> None:
|
||||
session = await self.memory.create_session(name=name)
|
||||
self.session_id = session.sid
|
||||
self.agent = self._make_agent()
|
||||
|
||||
async def resume_session(self, session_id: int) -> None:
|
||||
row = await self.memory.get_session(session_id)
|
||||
if row is None:
|
||||
msg = f"session not found: {session_id}"
|
||||
raise ValueError(msg)
|
||||
self.session_id = session_id
|
||||
self.agent = self._make_agent()
|
||||
await self.agent.load_history()
|
||||
Reference in New Issue
Block a user