mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +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