Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/gumloop/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

__version__ = "0.4.6"
__version__ = "0.4.7"
102 changes: 102 additions & 0 deletions src/gumloop/cli/commands/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,108 @@ def get_agent(
console.print(f" {agent.system_prompt}", markup=False, highlight=False)


@agents_app.command(
"versions",
epilog="Examples:\n gumloop agents versions agent_abc\n gumloop agents versions agent_abc --limit 50 --json",
)
def list_agent_versions(
ctx: typer.Context,
agent_id: Annotated[str, typer.Argument(help="ID of the agent whose versions should be listed.")],
limit: Annotated[
int | None,
typer.Option("--limit", help="Maximum number of versions to return."),
] = None,
cursor: Annotated[
str | None,
typer.Option("--cursor", help="Pagination cursor from a previous versions call."),
] = None,
json_output: Annotated[
bool,
typer.Option("--json", help="Print the raw SDK response as JSON."),
] = False,
) -> None:
"""List immutable versions of an agent."""
cli: CliContext = ctx.obj
try:
response = cli.call_with_refresh(
lambda client: client.agents.list_versions(
agent_id,
page_size=limit,
cursor=cursor,
team_id=cli.effective_team_id,
)
)
except GumloopError as error:
exit_with_error(error, json_output=json_output)

if json_output:
print_json(response)
return

if not response.versions:
console.print("No agent versions found.")
else:
console.print("ID", "VERSION", "NAME", "DEPLOYED", "CREATED", sep="\t", soft_wrap=True)
for version in response.versions:
console.print(
version.id,
str(version.major_version),
version.name,
"yes" if version.is_deployed else "no",
version.created_at or "",
sep="\t",
soft_wrap=True,
)

if response.next_cursor:
console.print(f"\n[dim]Next cursor:[/dim] {escape_markup(response.next_cursor)}")


@agents_app.command(
"export",
epilog=(
"Examples:\n"
" gumloop agents export agent_abc version_123\n"
" gumloop agents export agent_abc version_123 --output agent-version.json"
),
)
def export_agent_version(
ctx: typer.Context,
agent_id: Annotated[str, typer.Argument(help="ID of the agent to export.")],
version_id: Annotated[str, typer.Argument(help="ID of the immutable version to export.")],
output: Annotated[
str | None,
typer.Option("-o", "--output", help="File to write. Omit or use '-' for stdout."),
] = None,
) -> None:
"""Export one immutable agent version as JSON."""
cli: CliContext = ctx.obj
try:
response = cli.call_with_refresh(
lambda client: client.agents.get_version(
agent_id,
version_id,
team_id=cli.effective_team_id,
)
)
if output in (None, "-"):
print_json(response)
return

destination = Path(output).expanduser()
payload = json.dumps(response.model_dump(mode="json"), sort_keys=True)
destination.write_text(f"{payload}\n", encoding="utf-8")
except GumloopError as error:
exit_with_error(error, json_output=True)
except OSError as error:
exit_with_error(
GumloopError(f"Could not write {output}: {error.strerror or error}"),
json_output=True,
)

console.print(f"[green]Saved[/green] {destination}")


@agents_app.command(
"create",
epilog=(
Expand Down
58 changes: 58 additions & 0 deletions src/gumloop/resources/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from gumloop.types import AgentResponse
from gumloop.types import AgentSkillsResponse
from gumloop.types import AgentUpdateRequest
from gumloop.types import AgentVersionResponse
from gumloop.types import AgentVersionsResponse
from gumloop.types import EvaluationConfigResponse
from gumloop.types import EvaluationConfigUpdateRequest
from gumloop.types import EvaluationResultListResponse
Expand Down Expand Up @@ -55,6 +57,35 @@ def create(
def retrieve(self, agent_id: str) -> AgentResponse:
return AgentResponse.model_validate(self._client.get(f"agents/{agent_id}"))

def list_versions(
self,
agent_id: str,
*,
page_size: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
) -> AgentVersionsResponse:
return AgentVersionsResponse.model_validate(
self._client.get(
f"agents/{agent_id}/versions",
params={"page_size": page_size, "cursor": cursor, "team_id": team_id},
)
)

def get_version(
self,
agent_id: str,
version_id: str,
*,
team_id: str | None = None,
) -> AgentVersionResponse:
return AgentVersionResponse.model_validate(
self._client.get(
f"agents/{agent_id}/versions/{version_id}",
params={"team_id": team_id},
)
)

def update(
self,
agent_id: str,
Expand Down Expand Up @@ -179,6 +210,33 @@ async def create(
async def retrieve(self, agent_id: str) -> AgentResponse:
return AgentResponse.model_validate(await self._client.get(f"agents/{agent_id}"))

async def list_versions(
self,
agent_id: str,
*,
page_size: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
) -> AgentVersionsResponse:
data = await self._client.get(
f"agents/{agent_id}/versions",
params={"page_size": page_size, "cursor": cursor, "team_id": team_id},
)
return AgentVersionsResponse.model_validate(data)

async def get_version(
self,
agent_id: str,
version_id: str,
*,
team_id: str | None = None,
) -> AgentVersionResponse:
data = await self._client.get(
f"agents/{agent_id}/versions/{version_id}",
params={"team_id": team_id},
)
return AgentVersionResponse.model_validate(data)

async def update(
self,
agent_id: str,
Expand Down
97 changes: 97 additions & 0 deletions src/gumloop/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,103 @@ class AgentListResponse(_Model):
next_cursor: str | None = None


class AgentVersionKnowledgeSource(_Model):
connector_id: str
config: dict[str, Any] | None = None


class AgentVersionComposition(_Model):
complete: bool
schema_version: int | None = None
name: str
description: str | None = None
model_name: str
system_prompt: str | None = None
tools: list[dict[str, Any]] = Field(default_factory=list)
resources: list[dict[str, Any]] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
skill_ids: list[str] | None = None
knowledge_sources: list[AgentVersionKnowledgeSource] | None = None


class AgentVersionSummary(_Model):
id: str
agent_id: str
major_version: int
name: str
is_deployed: bool | None = None
created_at: str | None = None
creator: CreatorPayload | None = None


class AgentVersion(AgentVersionSummary):
composition: AgentVersionComposition


class AgentVersionTextHunk(_Model):
old_start: int
old_end: int
new_start: int
new_end: int
old_text: str
new_text: str


class AgentVersionFieldChange(_Model):
field: str
status: str
old_value: Any
new_value: Any


class AgentVersionTextFieldChange(_Model):
field: str
status: str
text_hunks: list[AgentVersionTextHunk]


class AgentVersionCollectionItemChange(_Model):
identity: dict[str, Any]
status: str
old_position: int | None = None
new_position: int | None = None
old_value: Any = None
new_value: Any = None
field_changes: list[AgentVersionFieldChange] = Field(default_factory=list)


class AgentVersionSkillChange(_Model):
skill_id: str
status: str


class AgentVersionKnowledgeSourceChange(_Model):
connector_id: str
status: str
old_config: Any
new_config: Any
field_changes: list[AgentVersionFieldChange] = Field(default_factory=list)


class AgentVersionChanges(_Model):
base_version_id: str
attachment_changes_complete: bool
field_changes: list[AgentVersionTextFieldChange | AgentVersionFieldChange] = Field(default_factory=list)
tool_changes: list[AgentVersionCollectionItemChange] = Field(default_factory=list)
skill_changes: list[AgentVersionSkillChange] = Field(default_factory=list)
knowledge_source_changes: list[AgentVersionKnowledgeSourceChange] = Field(default_factory=list)


class AgentVersionResponse(_Model):
version: AgentVersion
changes: AgentVersionChanges | None = None


class AgentVersionsResponse(_Model):
versions: list[AgentVersionSummary] = Field(default_factory=list)
next_cursor: str | None = None


# ---------------------------------------------------------------------------
# Model types
# ---------------------------------------------------------------------------
Expand Down
67 changes: 67 additions & 0 deletions tests/cli/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,73 @@ def test_agents_get_calls_per_agent_endpoint(cli_runner: CliRunner) -> None:
assert '"id": "agent_abc"' in result.output


@respx.mock
def test_agents_versions_lists_versions_with_pagination(cli_runner: CliRunner) -> None:
route = respx.get(f"{API_BASE}/agents/agent_abc/versions").mock(
return_value=httpx.Response(
200,
json={
"versions": [
{
"id": "version_2",
"agent_id": "agent_abc",
"major_version": 2,
"name": "Support Bot",
"is_deployed": True,
}
],
"next_cursor": "cursor_2",
},
)
)
save_credentials(Credentials(api_key="key"))

result = cli_runner.invoke(app, ["agents", "versions", "agent_abc", "--limit", "25", "--cursor", "cursor_1"])

assert result.exit_code == 0, result.output
assert "version_2" in result.output
assert "Support Bot" in result.output
assert "cursor_2" in result.output
assert route.calls[0].request.url.params["page_size"] == "25"
assert route.calls[0].request.url.params["cursor"] == "cursor_1"


@respx.mock
def test_agents_export_writes_version_response_as_json(cli_runner: CliRunner, tmp_path: Path) -> None:
respx.get(f"{API_BASE}/agents/agent_abc/versions/version_2").mock(
return_value=httpx.Response(
200,
json={
"version": {
"id": "version_2",
"agent_id": "agent_abc",
"major_version": 2,
"name": "Support Bot",
"composition": {
"complete": True,
"name": "Support Bot",
"model_name": "auto",
"system_prompt": "Be helpful.",
},
},
"changes": None,
},
)
)
save_credentials(Credentials(api_key="key"))
output_path = tmp_path / "agent-version.json"

result = cli_runner.invoke(
app,
["agents", "export", "agent_abc", "version_2", "--output", str(output_path)],
)

assert result.exit_code == 0, result.output
payload = json.loads(output_path.read_text(encoding="utf-8"))
assert payload["version"]["id"] == "version_2"
assert payload["version"]["composition"]["system_prompt"] == "Be helpful."


@respx.mock
def test_agents_create_posts_required_fields(cli_runner: CliRunner) -> None:
route = respx.post(f"{API_BASE}/agents").mock(
Expand Down
Loading