use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkConfig {
#[serde(default = "default_base_url")]
pub base_url: String,
#[serde(default = "default_package_name")]
pub package_name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default = "default_true")]
pub include_async: bool,
#[serde(default = "default_true")]
pub include_streaming: bool,
}
fn default_base_url() -> String {
"http://localhost:3000".to_string()
}
fn default_package_name() -> String {
"argentor_client".to_string()
}
fn default_version() -> String {
"0.1.0".to_string()
}
fn default_true() -> bool {
true
}
impl Default for SdkConfig {
fn default() -> Self {
Self {
base_url: default_base_url(),
package_name: default_package_name(),
version: default_version(),
include_async: true,
include_streaming: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedFile {
pub path: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkOutput {
pub language: String,
pub files: Vec<GeneratedFile>,
}
pub struct SdkGenerator;
impl SdkGenerator {
pub fn new() -> Self {
Self
}
pub fn generate_python(&self, config: &SdkConfig) -> SdkOutput {
let pkg = &config.package_name;
let mut files = vec![
GeneratedFile {
path: format!("{pkg}/__init__.py"),
content: generate_python_init(config),
},
GeneratedFile {
path: format!("{pkg}/client.py"),
content: generate_python_client(config),
},
GeneratedFile {
path: format!("{pkg}/models.py"),
content: generate_python_models(config),
},
GeneratedFile {
path: "setup.py".to_string(),
content: generate_python_setup(config),
},
GeneratedFile {
path: "README.md".to_string(),
content: generate_python_readme(config),
},
];
if config.include_streaming {
files.push(GeneratedFile {
path: format!("{pkg}/streaming.py"),
content: generate_python_streaming(config),
});
}
SdkOutput {
language: "python".to_string(),
files,
}
}
pub fn generate_typescript(&self, config: &SdkConfig) -> SdkOutput {
let mut files = vec![
GeneratedFile {
path: "src/index.ts".to_string(),
content: generate_ts_index(config),
},
GeneratedFile {
path: "src/types.ts".to_string(),
content: generate_ts_types(config),
},
GeneratedFile {
path: "package.json".to_string(),
content: generate_ts_package_json(config),
},
GeneratedFile {
path: "tsconfig.json".to_string(),
content: generate_ts_tsconfig(),
},
GeneratedFile {
path: "README.md".to_string(),
content: generate_ts_readme(config),
},
];
if config.include_streaming {
files.push(GeneratedFile {
path: "src/streaming.ts".to_string(),
content: generate_ts_streaming(config),
});
}
SdkOutput {
language: "typescript".to_string(),
files,
}
}
pub fn generate_all(&self, config: &SdkConfig) -> Vec<SdkOutput> {
vec![
self.generate_python(config),
self.generate_typescript(config),
]
}
}
impl Default for SdkGenerator {
fn default() -> Self {
Self::new()
}
}
fn generate_python_init(config: &SdkConfig) -> String {
let streaming_import = if config.include_streaming {
format!(
"from {pkg}.streaming import SSEStream\n",
pkg = config.package_name
)
} else {
String::new()
};
format!(
r#""""{name} — Python client for the Argentor API."""
from {pkg}.client import ArgentorClient
from {pkg}.models import (
RunTaskRequest,
RunTaskResponse,
BatchRequest,
BatchResponse,
EvaluateRequest,
EvaluateResponse,
CreatePersonaRequest,
CreatePersonaResponse,
ListPersonasResponse,
UsageBreakdown,
HealthResponse,
WebhookProxyRequest,
WebhookProxyResponse,
)
{streaming_import}
__version__ = "{version}"
__all__ = [
"ArgentorClient",
"RunTaskRequest",
"RunTaskResponse",
"BatchRequest",
"BatchResponse",
"EvaluateRequest",
"EvaluateResponse",
"CreatePersonaRequest",
"CreatePersonaResponse",
"ListPersonasResponse",
"UsageBreakdown",
"HealthResponse",
"WebhookProxyRequest",
"WebhookProxyResponse",
]
"#,
name = config.package_name,
pkg = config.package_name,
version = config.version,
)
}
fn generate_python_client(config: &SdkConfig) -> String {
let base_url = &config.base_url;
let async_methods = if config.include_async {
format!(
r#"
class AsyncArgentorClient:
"""Async client for the Argentor API."""
def __init__(
self,
base_url: str = "{base_url}",
api_key: str = "",
tenant_id: str = "",
timeout: float = 60.0,
):
self.base_url = base_url
self.headers = {{"X-API-Key": api_key, "X-Tenant-ID": tenant_id}}
self._client = httpx.AsyncClient(
base_url=base_url, headers=self.headers, timeout=timeout
)
async def close(self) -> None:
"""Close the underlying HTTP client."""
await self._client.aclose()
async def __aenter__(self) -> "AsyncArgentorClient":
return self
async def __aexit__(self, *exc) -> None:
await self.close()
async def run_task(
self,
agent_role: str,
context: str,
*,
model: Optional[str] = None,
max_tokens: Optional[int] = None,
tools: Optional[List[str]] = None,
) -> dict:
"""Execute a single agent task."""
payload: Dict[str, Any] = {{"agent_role": agent_role, "context": context}}
if model is not None:
payload["model"] = model
if max_tokens is not None:
payload["max_tokens"] = max_tokens
if tools is not None:
payload["tools"] = tools
resp = await self._client.post("/v1/run", json=payload)
resp.raise_for_status()
return resp.json()
async def run_task_stream(
self,
agent_role: str,
context: str,
*,
model: Optional[str] = None,
max_tokens: Optional[int] = None,
tools: Optional[List[str]] = None,
) -> AsyncIterator[dict]:
"""Stream task results via SSE."""
payload: Dict[str, Any] = {{"agent_role": agent_role, "context": context}}
if model is not None:
payload["model"] = model
if max_tokens is not None:
payload["max_tokens"] = max_tokens
if tools is not None:
payload["tools"] = tools
async with self._client.stream("POST", "/v1/run/stream", json=payload) as resp:
resp.raise_for_status()
async for line in resp.aiter_lines():
if line.startswith("data: "):
data = line[len("data: "):]
if data.strip() == "[DONE]":
break
yield json.loads(data)
async def batch(
self,
tasks: List[Dict[str, Any]],
*,
max_concurrent: int = 5,
) -> dict:
"""Submit a batch of tasks for parallel execution."""
payload = {{"tasks": tasks, "max_concurrent": max_concurrent}}
resp = await self._client.post("/v1/batch", json=payload)
resp.raise_for_status()
return resp.json()
async def evaluate(
self,
response: str,
context: str,
criteria: Optional[List[str]] = None,
) -> dict:
"""Evaluate an agent response against criteria."""
payload: Dict[str, Any] = {{"response": response, "context": context}}
if criteria is not None:
payload["criteria"] = criteria
resp = await self._client.post("/v1/evaluate", json=payload)
resp.raise_for_status()
return resp.json()
async def create_persona(
self,
tenant_id: str,
agent_role: str,
persona: Dict[str, Any],
) -> dict:
"""Create a new agent persona for a tenant."""
payload = {{"tenant_id": tenant_id, "agent_role": agent_role, "persona": persona}}
resp = await self._client.post("/v1/personas", json=payload)
resp.raise_for_status()
return resp.json()
async def list_personas(self, tenant_id: str) -> dict:
"""List all personas for a tenant."""
resp = await self._client.get("/v1/personas", params={{"tenant_id": tenant_id}})
resp.raise_for_status()
return resp.json()
async def get_usage(self, tenant_id: str) -> dict:
"""Get usage breakdown for a tenant."""
resp = await self._client.get(f"/v1/usage/{{tenant_id}}")
resp.raise_for_status()
return resp.json()
async def health(self) -> dict:
"""Check API server health."""
resp = await self._client.get("/health")
resp.raise_for_status()
return resp.json()
async def webhook_proxy(
self,
event: str,
data: Dict[str, Any],
*,
source: str = "",
secret: str = "",
) -> dict:
"""Forward a webhook event through the proxy."""
payload: Dict[str, Any] = {{"event": event, "data": data}}
if source:
payload["source"] = source
if secret:
payload["secret"] = secret
resp = await self._client.post("/v1/webhooks/proxy", json=payload)
resp.raise_for_status()
return resp.json()
"#
)
} else {
String::new()
};
let streaming_import = if config.include_streaming {
"import json\n"
} else {
""
};
let stream_method = if config.include_streaming {
r#"
def run_task_stream(
self,
agent_role: str,
context: str,
*,
model: Optional[str] = None,
max_tokens: Optional[int] = None,
tools: Optional[List[str]] = None,
) -> Iterator[dict]:
"""Stream task results via SSE (synchronous)."""
payload: Dict[str, Any] = {"agent_role": agent_role, "context": context}
if model is not None:
payload["model"] = model
if max_tokens is not None:
payload["max_tokens"] = max_tokens
if tools is not None:
payload["tools"] = tools
with self._client.stream("POST", "/v1/run/stream", json=payload) as resp:
resp.raise_for_status()
for line in resp.iter_lines():
if line.startswith("data: "):
data = line[len("data: "):]
if data.strip() == "[DONE]":
break
yield json.loads(data)
"#
} else {
""
};
format!(
r#""""{name} — Python client for the Argentor API."""
import httpx
{streaming_import}from typing import Optional, Dict, List, Any, Iterator, AsyncIterator
class ArgentorClient:
"""Synchronous client for the Argentor API."""
def __init__(
self,
base_url: str = "{base_url}",
api_key: str = "",
tenant_id: str = "",
timeout: float = 60.0,
):
self.base_url = base_url
self.headers = {{"X-API-Key": api_key, "X-Tenant-ID": tenant_id}}
self._client = httpx.Client(
base_url=base_url, headers=self.headers, timeout=timeout
)
def close(self) -> None:
"""Close the underlying HTTP client."""
self._client.close()
def __enter__(self) -> "ArgentorClient":
return self
def __exit__(self, *exc) -> None:
self.close()
def run_task(
self,
agent_role: str,
context: str,
*,
model: Optional[str] = None,
max_tokens: Optional[int] = None,
tools: Optional[List[str]] = None,
) -> dict:
"""Execute a single agent task."""
payload: Dict[str, Any] = {{"agent_role": agent_role, "context": context}}
if model is not None:
payload["model"] = model
if max_tokens is not None:
payload["max_tokens"] = max_tokens
if tools is not None:
payload["tools"] = tools
resp = self._client.post("/v1/run", json=payload)
resp.raise_for_status()
return resp.json()
{stream_method}
def batch(
self,
tasks: List[Dict[str, Any]],
*,
max_concurrent: int = 5,
) -> dict:
"""Submit a batch of tasks for parallel execution."""
payload = {{"tasks": tasks, "max_concurrent": max_concurrent}}
resp = self._client.post("/v1/batch", json=payload)
resp.raise_for_status()
return resp.json()
def evaluate(
self,
response: str,
context: str,
criteria: Optional[List[str]] = None,
) -> dict:
"""Evaluate an agent response against criteria."""
payload: Dict[str, Any] = {{"response": response, "context": context}}
if criteria is not None:
payload["criteria"] = criteria
resp = self._client.post("/v1/evaluate", json=payload)
resp.raise_for_status()
return resp.json()
def create_persona(
self,
tenant_id: str,
agent_role: str,
persona: Dict[str, Any],
) -> dict:
"""Create a new agent persona for a tenant."""
payload = {{"tenant_id": tenant_id, "agent_role": agent_role, "persona": persona}}
resp = self._client.post("/v1/personas", json=payload)
resp.raise_for_status()
return resp.json()
def list_personas(self, tenant_id: str) -> dict:
"""List all personas for a tenant."""
resp = self._client.get("/v1/personas", params={{"tenant_id": tenant_id}})
resp.raise_for_status()
return resp.json()
def get_usage(self, tenant_id: str) -> dict:
"""Get usage breakdown for a tenant."""
resp = self._client.get(f"/v1/usage/{{tenant_id}}")
resp.raise_for_status()
return resp.json()
def health(self) -> dict:
"""Check API server health."""
resp = self._client.get("/health")
resp.raise_for_status()
return resp.json()
def webhook_proxy(
self,
event: str,
data: Dict[str, Any],
*,
source: str = "",
secret: str = "",
) -> dict:
"""Forward a webhook event through the proxy."""
payload: Dict[str, Any] = {{"event": event, "data": data}}
if source:
payload["source"] = source
if secret:
payload["secret"] = secret
resp = self._client.post("/v1/webhooks/proxy", json=payload)
resp.raise_for_status()
return resp.json()
{async_methods}"#,
name = config.package_name,
base_url = base_url,
)
}
fn generate_python_models(_config: &SdkConfig) -> String {
r#""""Pydantic models for Argentor API request/response types."""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Run Task
# ---------------------------------------------------------------------------
class RunTaskRequest(BaseModel):
"""Request body for POST /v1/run."""
agent_role: str
context: str
model: Optional[str] = None
max_tokens: Optional[int] = None
tools: Optional[List[str]] = None
class RunTaskResponse(BaseModel):
"""Response from POST /v1/run."""
task_id: str
status: str
result: Optional[str] = None
tokens_used: Optional[int] = None
duration_ms: Optional[int] = None
metadata: Optional[Dict[str, Any]] = None
# ---------------------------------------------------------------------------
# Batch
# ---------------------------------------------------------------------------
class BatchTask(BaseModel):
"""A single task within a batch request."""
agent_role: str
context: str
model: Optional[str] = None
max_tokens: Optional[int] = None
class BatchRequest(BaseModel):
"""Request body for POST /v1/batch."""
tasks: List[BatchTask]
max_concurrent: int = Field(default=5, ge=1, le=50)
class BatchTaskResult(BaseModel):
"""Result for one task in a batch."""
task_id: str
status: str
result: Optional[str] = None
error: Optional[str] = None
class BatchResponse(BaseModel):
"""Response from POST /v1/batch."""
batch_id: str
results: List[BatchTaskResult]
total: int
succeeded: int
failed: int
# ---------------------------------------------------------------------------
# Evaluate
# ---------------------------------------------------------------------------
class EvaluateRequest(BaseModel):
"""Request body for POST /v1/evaluate."""
response: str
context: str
criteria: Optional[List[str]] = None
class CriterionScore(BaseModel):
"""Score for a single evaluation criterion."""
criterion: str
score: float = Field(ge=0.0, le=1.0)
explanation: Optional[str] = None
class EvaluateResponse(BaseModel):
"""Response from POST /v1/evaluate."""
overall_score: float = Field(ge=0.0, le=1.0)
scores: List[CriterionScore]
summary: Optional[str] = None
# ---------------------------------------------------------------------------
# Personas
# ---------------------------------------------------------------------------
class PersonaConfig(BaseModel):
"""Configuration for an agent persona."""
name: str
system_prompt: Optional[str] = None
temperature: Optional[float] = None
model: Optional[str] = None
tools: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
class CreatePersonaRequest(BaseModel):
"""Request body for POST /v1/personas."""
tenant_id: str
agent_role: str
persona: PersonaConfig
class CreatePersonaResponse(BaseModel):
"""Response from POST /v1/personas."""
persona_id: str
tenant_id: str
agent_role: str
created_at: str
class PersonaSummary(BaseModel):
"""Summary of a persona in list responses."""
persona_id: str
agent_role: str
name: str
created_at: str
class ListPersonasResponse(BaseModel):
"""Response from GET /v1/personas."""
tenant_id: str
personas: List[PersonaSummary]
# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------
class ModelUsage(BaseModel):
"""Token usage for a single model."""
model: str
input_tokens: int
output_tokens: int
total_tokens: int
cost_usd: Optional[float] = None
class UsageBreakdown(BaseModel):
"""Response from GET /v1/usage/{tenant_id}."""
tenant_id: str
period_start: str
period_end: str
models: List[ModelUsage]
total_tokens: int
total_cost_usd: Optional[float] = None
# ---------------------------------------------------------------------------
# Health
# ---------------------------------------------------------------------------
class HealthResponse(BaseModel):
"""Response from GET /health."""
status: str
version: str
uptime_seconds: Optional[float] = None
# ---------------------------------------------------------------------------
# Webhook Proxy
# ---------------------------------------------------------------------------
class WebhookProxyRequest(BaseModel):
"""Request body for POST /v1/webhooks/proxy."""
event: str
data: Dict[str, Any]
source: Optional[str] = None
secret: Optional[str] = None
class WebhookProxyResponse(BaseModel):
"""Response from POST /v1/webhooks/proxy."""
accepted: bool
event_id: Optional[str] = None
message: Optional[str] = None
"#
.to_string()
}
fn generate_python_streaming(config: &SdkConfig) -> String {
format!(
r#""""{name} — SSE streaming helpers."""
import json
from typing import Iterator, AsyncIterator, Any
class SSEStream:
"""Parses a Server-Sent Events stream from raw lines."""
def __init__(self, lines: Iterator[str]):
self._lines = lines
def __iter__(self) -> Iterator[dict]:
for line in self._lines:
if line.startswith("data: "):
data = line[len("data: "):]
stripped = data.strip()
if stripped == "[DONE]":
return
yield json.loads(stripped)
class AsyncSSEStream:
"""Parses a Server-Sent Events stream from async lines."""
def __init__(self, lines: AsyncIterator[str]):
self._lines = lines
async def __aiter__(self) -> AsyncIterator[dict]:
async for line in self._lines:
if line.startswith("data: "):
data = line[len("data: "):]
stripped = data.strip()
if stripped == "[DONE]":
return
yield json.loads(stripped)
"#,
name = config.package_name,
)
}
fn generate_python_setup(config: &SdkConfig) -> String {
format!(
r#"from setuptools import setup, find_packages
setup(
name="{name}",
version="{version}",
description="Python SDK client for the Argentor API",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
packages=find_packages(),
python_requires=">=3.9",
install_requires=[
"httpx>=0.25.0",
"pydantic>=2.0.0",
],
extras_require={{
"dev": [
"pytest>=7.0",
"pytest-asyncio>=0.21",
"respx>=0.20",
],
}},
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)
"#,
name = config.package_name,
version = config.version,
)
}
fn generate_python_readme(config: &SdkConfig) -> String {
format!(
r#"# {name}
Python SDK client for the Argentor API.
## Installation
```bash
pip install {name}
```
## Quick Start
```python
from {name} import ArgentorClient
client = ArgentorClient(
base_url="{base_url}",
api_key="your-api-key",
tenant_id="your-tenant-id",
)
# Run a task
result = client.run_task(
agent_role="code_reviewer",
context="Review the following pull request...",
)
print(result)
# Stream results
for token in client.run_task_stream(
agent_role="assistant",
context="Explain how Argentor works",
):
print(token, end="", flush=True)
# Batch execution
batch_result = client.batch(
tasks=[
{{"agent_role": "analyst", "context": "Analyze Q1 sales data"}},
{{"agent_role": "analyst", "context": "Analyze Q2 sales data"}},
],
max_concurrent=5,
)
# Evaluate a response
evaluation = client.evaluate(
response="The code looks clean and follows best practices.",
context="Code review task",
criteria=["accuracy", "completeness", "helpfulness"],
)
# Health check
health = client.health()
print(health)
```
## Async Usage
```python
import asyncio
from {name}.client import AsyncArgentorClient
async def main():
async with AsyncArgentorClient(
base_url="{base_url}",
api_key="your-api-key",
) as client:
result = await client.run_task(
agent_role="assistant",
context="Hello, world!",
)
print(result)
asyncio.run(main())
```
## API Reference
### ArgentorClient
| Method | Description |
|--------|-------------|
| `run_task(agent_role, context, **kwargs)` | Execute a single agent task |
| `run_task_stream(agent_role, context, **kwargs)` | Stream task results via SSE |
| `batch(tasks, max_concurrent=5)` | Submit a batch of tasks |
| `evaluate(response, context, criteria)` | Evaluate an agent response |
| `create_persona(tenant_id, agent_role, persona)` | Create a new persona |
| `list_personas(tenant_id)` | List personas for a tenant |
| `get_usage(tenant_id)` | Get usage breakdown |
| `health()` | Check API health |
| `webhook_proxy(event, data, source, secret)` | Forward a webhook event |
"#,
name = config.package_name,
base_url = config.base_url,
)
}
fn generate_ts_index(config: &SdkConfig) -> String {
let base_url = &config.base_url;
let stream_import = if config.include_streaming {
"import { parseSSEStream } from './streaming';\n"
} else {
""
};
let stream_method = if config.include_streaming {
r#"
/**
* Stream task results via SSE.
*/
async *runTaskStream(
agentRole: string,
context: string,
options?: { model?: string; maxTokens?: number; tools?: string[] },
): AsyncGenerator<Record<string, unknown>> {
const payload: Record<string, unknown> = { agent_role: agentRole, context };
if (options?.model) payload.model = options.model;
if (options?.maxTokens) payload.max_tokens = options.maxTokens;
if (options?.tools) payload.tools = options.tools;
const resp = await fetch(`${this.baseUrl}/v1/run/stream`, {
method: 'POST',
headers: { ...this.headers, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
yield* parseSSEStream(resp);
}
"#
} else {
""
};
let async_qualifier = if config.include_async {
""
} else {
"// Note: async methods omitted per configuration\n"
};
format!(
r#"/**
* {name} — TypeScript client for the Argentor API.
*/
import type {{
RunTaskResponse,
BatchResponse,
EvaluateResponse,
CreatePersonaResponse,
ListPersonasResponse,
UsageBreakdown,
HealthResponse,
WebhookProxyResponse,
}} from './types';
{stream_import}{async_qualifier}
export class ArgentorClient {{
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
constructor(options?: {{
baseUrl?: string;
apiKey?: string;
tenantId?: string;
}}) {{
this.baseUrl = options?.baseUrl ?? '{base_url}';
this.headers = {{
'Content-Type': 'application/json',
'X-API-Key': options?.apiKey ?? '',
'X-Tenant-ID': options?.tenantId ?? '',
}};
}}
/**
* Execute a single agent task.
*/
async runTask(
agentRole: string,
context: string,
options?: {{ model?: string; maxTokens?: number; tools?: string[] }},
): Promise<RunTaskResponse> {{
const payload: Record<string, unknown> = {{ agent_role: agentRole, context }};
if (options?.model) payload.model = options.model;
if (options?.maxTokens) payload.max_tokens = options.maxTokens;
if (options?.tools) payload.tools = options.tools;
const resp = await fetch(`${{this.baseUrl}}/v1/run`, {{
method: 'POST',
headers: this.headers,
body: JSON.stringify(payload),
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<RunTaskResponse>;
}}
{stream_method}
/**
* Submit a batch of tasks for parallel execution.
*/
async batch(
tasks: Array<{{ agentRole: string; context: string; model?: string; maxTokens?: number }}>,
options?: {{ maxConcurrent?: number }},
): Promise<BatchResponse> {{
const payload = {{
tasks: tasks.map((t) => ({{
agent_role: t.agentRole,
context: t.context,
model: t.model,
max_tokens: t.maxTokens,
}})),
max_concurrent: options?.maxConcurrent ?? 5,
}};
const resp = await fetch(`${{this.baseUrl}}/v1/batch`, {{
method: 'POST',
headers: this.headers,
body: JSON.stringify(payload),
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<BatchResponse>;
}}
/**
* Evaluate an agent response against criteria.
*/
async evaluate(
response: string,
context: string,
criteria?: string[],
): Promise<EvaluateResponse> {{
const payload: Record<string, unknown> = {{ response, context }};
if (criteria) payload.criteria = criteria;
const resp = await fetch(`${{this.baseUrl}}/v1/evaluate`, {{
method: 'POST',
headers: this.headers,
body: JSON.stringify(payload),
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<EvaluateResponse>;
}}
/**
* Create a new agent persona for a tenant.
*/
async createPersona(
tenantId: string,
agentRole: string,
persona: Record<string, unknown>,
): Promise<CreatePersonaResponse> {{
const payload = {{ tenant_id: tenantId, agent_role: agentRole, persona }};
const resp = await fetch(`${{this.baseUrl}}/v1/personas`, {{
method: 'POST',
headers: this.headers,
body: JSON.stringify(payload),
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<CreatePersonaResponse>;
}}
/**
* List all personas for a tenant.
*/
async listPersonas(tenantId: string): Promise<ListPersonasResponse> {{
const resp = await fetch(
`${{this.baseUrl}}/v1/personas?tenant_id=${{encodeURIComponent(tenantId)}}`,
{{ headers: this.headers }},
);
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<ListPersonasResponse>;
}}
/**
* Get usage breakdown for a tenant.
*/
async getUsage(tenantId: string): Promise<UsageBreakdown> {{
const resp = await fetch(
`${{this.baseUrl}}/v1/usage/${{encodeURIComponent(tenantId)}}`,
{{ headers: this.headers }},
);
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<UsageBreakdown>;
}}
/**
* Check API server health.
*/
async health(): Promise<HealthResponse> {{
const resp = await fetch(`${{this.baseUrl}}/health`, {{
headers: this.headers,
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<HealthResponse>;
}}
/**
* Forward a webhook event through the proxy.
*/
async webhookProxy(
event: string,
data: Record<string, unknown>,
options?: {{ source?: string; secret?: string }},
): Promise<WebhookProxyResponse> {{
const payload: Record<string, unknown> = {{ event, data }};
if (options?.source) payload.source = options.source;
if (options?.secret) payload.secret = options.secret;
const resp = await fetch(`${{this.baseUrl}}/v1/webhooks/proxy`, {{
method: 'POST',
headers: this.headers,
body: JSON.stringify(payload),
}});
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}: ${{resp.statusText}}`);
return resp.json() as Promise<WebhookProxyResponse>;
}}
}}
export {{ ArgentorClient as default }};
export type * from './types';
"#,
name = config.package_name,
base_url = base_url,
)
}
fn generate_ts_types(_config: &SdkConfig) -> String {
r#"/**
* TypeScript type definitions for the Argentor API.
*/
// ---------------------------------------------------------------------------
// Run Task
// ---------------------------------------------------------------------------
export interface RunTaskRequest {
agent_role: string;
context: string;
model?: string;
max_tokens?: number;
tools?: string[];
}
export interface RunTaskResponse {
task_id: string;
status: string;
result?: string;
tokens_used?: number;
duration_ms?: number;
metadata?: Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// Batch
// ---------------------------------------------------------------------------
export interface BatchTask {
agent_role: string;
context: string;
model?: string;
max_tokens?: number;
}
export interface BatchRequest {
tasks: BatchTask[];
max_concurrent: number;
}
export interface BatchTaskResult {
task_id: string;
status: string;
result?: string;
error?: string;
}
export interface BatchResponse {
batch_id: string;
results: BatchTaskResult[];
total: number;
succeeded: number;
failed: number;
}
// ---------------------------------------------------------------------------
// Evaluate
// ---------------------------------------------------------------------------
export interface EvaluateRequest {
response: string;
context: string;
criteria?: string[];
}
export interface CriterionScore {
criterion: string;
score: number;
explanation?: string;
}
export interface EvaluateResponse {
overall_score: number;
scores: CriterionScore[];
summary?: string;
}
// ---------------------------------------------------------------------------
// Personas
// ---------------------------------------------------------------------------
export interface PersonaConfig {
name: string;
system_prompt?: string;
temperature?: number;
model?: string;
tools?: string[];
metadata?: Record<string, unknown>;
}
export interface CreatePersonaRequest {
tenant_id: string;
agent_role: string;
persona: PersonaConfig;
}
export interface CreatePersonaResponse {
persona_id: string;
tenant_id: string;
agent_role: string;
created_at: string;
}
export interface PersonaSummary {
persona_id: string;
agent_role: string;
name: string;
created_at: string;
}
export interface ListPersonasResponse {
tenant_id: string;
personas: PersonaSummary[];
}
// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------
export interface ModelUsage {
model: string;
input_tokens: number;
output_tokens: number;
total_tokens: number;
cost_usd?: number;
}
export interface UsageBreakdown {
tenant_id: string;
period_start: string;
period_end: string;
models: ModelUsage[];
total_tokens: number;
total_cost_usd?: number;
}
// ---------------------------------------------------------------------------
// Health
// ---------------------------------------------------------------------------
export interface HealthResponse {
status: string;
version: string;
uptime_seconds?: number;
}
// ---------------------------------------------------------------------------
// Webhook Proxy
// ---------------------------------------------------------------------------
export interface WebhookProxyRequest {
event: string;
data: Record<string, unknown>;
source?: string;
secret?: string;
}
export interface WebhookProxyResponse {
accepted: boolean;
event_id?: string;
message?: string;
}
"#
.to_string()
}
fn generate_ts_streaming(_config: &SdkConfig) -> String {
r#"/**
* SSE streaming helpers for the Argentor TypeScript SDK.
*/
/**
* Parse a Server-Sent Events response into an async generator of parsed data
* objects. Stops when it encounters a `data: [DONE]` message.
*/
export async function* parseSSEStream(
response: Response,
): AsyncGenerator<Record<string, unknown>> {
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Response body is not readable');
}
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('data: ')) {
const data = trimmed.slice('data: '.length).trim();
if (data === '[DONE]') {
return;
}
yield JSON.parse(data) as Record<string, unknown>;
}
}
}
// Process any remaining buffer content
if (buffer.trim().startsWith('data: ')) {
const data = buffer.trim().slice('data: '.length).trim();
if (data && data !== '[DONE]') {
yield JSON.parse(data) as Record<string, unknown>;
}
}
} finally {
reader.releaseLock();
}
}
"#
.to_string()
}
fn generate_ts_package_json(config: &SdkConfig) -> String {
format!(
r#"{{
"name": "{name}",
"version": "{version}",
"description": "TypeScript SDK client for the Argentor API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {{
"build": "tsc",
"clean": "rm -rf dist",
"prepublishOnly": "npm run build",
"test": "vitest run",
"lint": "eslint src/"
}},
"keywords": [
"argentor",
"ai",
"agent",
"sdk",
"client"
],
"license": "AGPL-3.0-only",
"devDependencies": {{
"typescript": "^5.4.0",
"vitest": "^1.6.0",
"eslint": "^9.0.0"
}}
}}
"#,
name = config.package_name,
version = config.version,
)
}
fn generate_ts_tsconfig() -> String {
r#"{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
"#
.to_string()
}
fn generate_ts_readme(config: &SdkConfig) -> String {
format!(
r#"# {name}
TypeScript SDK client for the Argentor API.
## Installation
```bash
npm install {name}
```
## Quick Start
```typescript
import {{ ArgentorClient }} from '{name}';
const client = new ArgentorClient({{
baseUrl: '{base_url}',
apiKey: 'your-api-key',
tenantId: 'your-tenant-id',
}});
// Run a task
const result = await client.runTask(
'code_reviewer',
'Review the following pull request...',
);
console.log(result);
// Stream results
for await (const chunk of client.runTaskStream(
'assistant',
'Explain how Argentor works',
)) {{
process.stdout.write(JSON.stringify(chunk));
}}
// Batch execution
const batchResult = await client.batch([
{{ agentRole: 'analyst', context: 'Analyze Q1 sales data' }},
{{ agentRole: 'analyst', context: 'Analyze Q2 sales data' }},
]);
// Evaluate a response
const evaluation = await client.evaluate(
'The code looks clean and follows best practices.',
'Code review task',
['accuracy', 'completeness', 'helpfulness'],
);
// Health check
const health = await client.health();
console.log(health);
```
## API Reference
### ArgentorClient
| Method | Description |
|--------|-------------|
| `runTask(agentRole, context, options?)` | Execute a single agent task |
| `runTaskStream(agentRole, context, options?)` | Stream task results via SSE |
| `batch(tasks, options?)` | Submit a batch of tasks |
| `evaluate(response, context, criteria?)` | Evaluate an agent response |
| `createPersona(tenantId, agentRole, persona)` | Create a new persona |
| `listPersonas(tenantId)` | List personas for a tenant |
| `getUsage(tenantId)` | Get usage breakdown |
| `health()` | Check API health |
| `webhookProxy(event, data, options?)` | Forward a webhook event |
"#,
name = config.package_name,
base_url = config.base_url,
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn default_config() -> SdkConfig {
SdkConfig::default()
}
#[test]
fn test_sdk_config_defaults() {
let cfg = SdkConfig::default();
assert_eq!(cfg.base_url, "http://localhost:3000");
assert_eq!(cfg.package_name, "argentor_client");
assert_eq!(cfg.version, "0.1.0");
assert!(cfg.include_async);
assert!(cfg.include_streaming);
}
#[test]
fn test_sdk_config_custom_values() {
let cfg = SdkConfig {
base_url: "https://api.example.com".to_string(),
package_name: "my_sdk".to_string(),
version: "2.0.0".to_string(),
include_async: false,
include_streaming: false,
};
assert_eq!(cfg.base_url, "https://api.example.com");
assert_eq!(cfg.package_name, "my_sdk");
assert_eq!(cfg.version, "2.0.0");
assert!(!cfg.include_async);
assert!(!cfg.include_streaming);
}
#[test]
fn test_sdk_generator_new() {
let _gen = SdkGenerator::new();
}
#[test]
fn test_sdk_generator_default() {
let _gen = SdkGenerator;
}
#[test]
fn test_python_sdk_file_count_with_streaming() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
assert_eq!(output.language, "python");
assert_eq!(output.files.len(), 6);
}
#[test]
fn test_python_sdk_file_count_without_streaming() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
include_streaming: false,
..default_config()
};
let output = gen.generate_python(&cfg);
assert_eq!(output.files.len(), 5);
}
#[test]
fn test_python_sdk_all_file_paths_present() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let paths: Vec<&str> = output.files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"argentor_client/__init__.py"));
assert!(paths.contains(&"argentor_client/client.py"));
assert!(paths.contains(&"argentor_client/models.py"));
assert!(paths.contains(&"argentor_client/streaming.py"));
assert!(paths.contains(&"setup.py"));
assert!(paths.contains(&"README.md"));
}
#[test]
fn test_python_init_exports_argentor_client() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let init = output
.files
.iter()
.find(|f| f.path.ends_with("__init__.py"))
.unwrap();
assert!(init
.content
.contains("from argentor_client.client import ArgentorClient"));
assert!(init.content.contains("__version__"));
}
#[test]
fn test_python_client_has_all_methods() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let client = output
.files
.iter()
.find(|f| f.path.ends_with("client.py"))
.unwrap();
assert!(client.content.contains("def run_task("));
assert!(client.content.contains("def run_task_stream("));
assert!(client.content.contains("def batch("));
assert!(client.content.contains("def evaluate("));
assert!(client.content.contains("def create_persona("));
assert!(client.content.contains("def list_personas("));
assert!(client.content.contains("def get_usage("));
assert!(client.content.contains("def health("));
assert!(client.content.contains("def webhook_proxy("));
}
#[test]
fn test_python_client_imports_httpx() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let client = output
.files
.iter()
.find(|f| f.path.ends_with("client.py"))
.unwrap();
assert!(client.content.contains("import httpx"));
}
#[test]
fn test_python_client_has_async_client() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let client = output
.files
.iter()
.find(|f| f.path.ends_with("client.py"))
.unwrap();
assert!(client.content.contains("class AsyncArgentorClient:"));
}
#[test]
fn test_python_client_no_async_when_disabled() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
include_async: false,
..default_config()
};
let output = gen.generate_python(&cfg);
let client = output
.files
.iter()
.find(|f| f.path.ends_with("client.py"))
.unwrap();
assert!(!client.content.contains("class AsyncArgentorClient:"));
}
#[test]
fn test_python_models_has_pydantic_classes() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let models = output
.files
.iter()
.find(|f| f.path.ends_with("models.py"))
.unwrap();
assert!(models.content.contains("class RunTaskRequest(BaseModel):"));
assert!(models.content.contains("class RunTaskResponse(BaseModel):"));
assert!(models.content.contains("class BatchRequest(BaseModel):"));
assert!(models.content.contains("class BatchResponse(BaseModel):"));
assert!(models.content.contains("class EvaluateRequest(BaseModel):"));
assert!(models
.content
.contains("class EvaluateResponse(BaseModel):"));
assert!(models
.content
.contains("class CreatePersonaRequest(BaseModel):"));
assert!(models
.content
.contains("class CreatePersonaResponse(BaseModel):"));
assert!(models
.content
.contains("class ListPersonasResponse(BaseModel):"));
assert!(models.content.contains("class UsageBreakdown(BaseModel):"));
assert!(models.content.contains("class HealthResponse(BaseModel):"));
assert!(models
.content
.contains("class WebhookProxyRequest(BaseModel):"));
assert!(models
.content
.contains("class WebhookProxyResponse(BaseModel):"));
}
#[test]
fn test_python_setup_has_package_metadata() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let setup = output.files.iter().find(|f| f.path == "setup.py").unwrap();
assert!(setup.content.contains("name=\"argentor_client\""));
assert!(setup.content.contains("version=\"0.1.0\""));
assert!(setup.content.contains("httpx"));
assert!(setup.content.contains("pydantic"));
}
#[test]
fn test_python_client_uses_custom_base_url() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
base_url: "https://my-api.example.com:8080".to_string(),
..default_config()
};
let output = gen.generate_python(&cfg);
let client = output
.files
.iter()
.find(|f| f.path.ends_with("client.py"))
.unwrap();
assert!(client.content.contains("https://my-api.example.com:8080"));
}
#[test]
fn test_python_streaming_helper() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let streaming = output
.files
.iter()
.find(|f| f.path.ends_with("streaming.py"))
.unwrap();
assert!(streaming.content.contains("class SSEStream:"));
assert!(streaming.content.contains("class AsyncSSEStream:"));
assert!(streaming.content.contains("[DONE]"));
}
#[test]
fn test_typescript_sdk_file_count_with_streaming() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
assert_eq!(output.language, "typescript");
assert_eq!(output.files.len(), 6);
}
#[test]
fn test_typescript_sdk_file_count_without_streaming() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
include_streaming: false,
..default_config()
};
let output = gen.generate_typescript(&cfg);
assert_eq!(output.files.len(), 5);
}
#[test]
fn test_typescript_sdk_all_file_paths_present() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let paths: Vec<&str> = output.files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"src/index.ts"));
assert!(paths.contains(&"src/types.ts"));
assert!(paths.contains(&"src/streaming.ts"));
assert!(paths.contains(&"package.json"));
assert!(paths.contains(&"tsconfig.json"));
assert!(paths.contains(&"README.md"));
}
#[test]
fn test_typescript_index_has_all_methods() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let index = output
.files
.iter()
.find(|f| f.path == "src/index.ts")
.unwrap();
assert!(index.content.contains("async runTask("));
assert!(index.content.contains("runTaskStream("));
assert!(index.content.contains("async batch("));
assert!(index.content.contains("async evaluate("));
assert!(index.content.contains("async createPersona("));
assert!(index.content.contains("async listPersonas("));
assert!(index.content.contains("async getUsage("));
assert!(index.content.contains("async health("));
assert!(index.content.contains("async webhookProxy("));
}
#[test]
fn test_typescript_types_has_all_interfaces() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let types = output
.files
.iter()
.find(|f| f.path == "src/types.ts")
.unwrap();
assert!(types.content.contains("export interface RunTaskRequest"));
assert!(types.content.contains("export interface RunTaskResponse"));
assert!(types.content.contains("export interface BatchRequest"));
assert!(types.content.contains("export interface BatchResponse"));
assert!(types.content.contains("export interface EvaluateRequest"));
assert!(types.content.contains("export interface EvaluateResponse"));
assert!(types
.content
.contains("export interface CreatePersonaRequest"));
assert!(types
.content
.contains("export interface CreatePersonaResponse"));
assert!(types
.content
.contains("export interface ListPersonasResponse"));
assert!(types.content.contains("export interface UsageBreakdown"));
assert!(types.content.contains("export interface HealthResponse"));
assert!(types
.content
.contains("export interface WebhookProxyRequest"));
assert!(types
.content
.contains("export interface WebhookProxyResponse"));
}
#[test]
fn test_typescript_package_json_metadata() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let pkg = output
.files
.iter()
.find(|f| f.path == "package.json")
.unwrap();
assert!(pkg.content.contains("\"name\": \"argentor_client\""));
assert!(pkg.content.contains("\"version\": \"0.1.0\""));
assert!(pkg.content.contains("typescript"));
}
#[test]
fn test_typescript_tsconfig() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let tsconfig = output
.files
.iter()
.find(|f| f.path == "tsconfig.json")
.unwrap();
assert!(tsconfig.content.contains("\"strict\": true"));
assert!(tsconfig.content.contains("\"declaration\": true"));
assert!(tsconfig.content.contains("\"outDir\": \"dist\""));
}
#[test]
fn test_typescript_streaming_helper() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
let streaming = output
.files
.iter()
.find(|f| f.path == "src/streaming.ts")
.unwrap();
assert!(streaming.content.contains("parseSSEStream"));
assert!(streaming.content.contains("AsyncGenerator"));
assert!(streaming.content.contains("[DONE]"));
}
#[test]
fn test_generate_all_returns_both_languages() {
let gen = SdkGenerator::new();
let outputs = gen.generate_all(&default_config());
assert_eq!(outputs.len(), 2);
let languages: Vec<&str> = outputs.iter().map(|o| o.language.as_str()).collect();
assert!(languages.contains(&"python"));
assert!(languages.contains(&"typescript"));
}
#[test]
fn test_generate_all_total_file_count() {
let gen = SdkGenerator::new();
let outputs = gen.generate_all(&default_config());
let total_files: usize = outputs.iter().map(|o| o.files.len()).sum();
assert_eq!(total_files, 12);
}
#[test]
fn test_custom_package_name_python() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
package_name: "my_custom_sdk".to_string(),
..default_config()
};
let output = gen.generate_python(&cfg);
let paths: Vec<&str> = output.files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"my_custom_sdk/__init__.py"));
assert!(paths.contains(&"my_custom_sdk/client.py"));
assert!(paths.contains(&"my_custom_sdk/models.py"));
}
#[test]
fn test_custom_package_name_typescript() {
let gen = SdkGenerator::new();
let cfg = SdkConfig {
package_name: "my_custom_sdk".to_string(),
..default_config()
};
let output = gen.generate_typescript(&cfg);
let pkg = output
.files
.iter()
.find(|f| f.path == "package.json")
.unwrap();
assert!(pkg.content.contains("\"name\": \"my_custom_sdk\""));
}
#[test]
fn test_sdk_output_serialization() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
let json_str = serde_json::to_string(&output).unwrap();
let deserialized: SdkOutput = serde_json::from_str(&json_str).unwrap();
assert_eq!(deserialized.language, "python");
assert_eq!(deserialized.files.len(), output.files.len());
}
#[test]
fn test_sdk_config_json_deserialization() {
let json = r#"{"base_url":"https://example.com","package_name":"test_sdk","version":"1.0.0","include_async":false,"include_streaming":true}"#;
let cfg: SdkConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.base_url, "https://example.com");
assert_eq!(cfg.package_name, "test_sdk");
assert_eq!(cfg.version, "1.0.0");
assert!(!cfg.include_async);
assert!(cfg.include_streaming);
}
#[test]
fn test_sdk_config_json_defaults() {
let json = r#"{}"#;
let cfg: SdkConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.base_url, "http://localhost:3000");
assert_eq!(cfg.package_name, "argentor_client");
assert_eq!(cfg.version, "0.1.0");
assert!(cfg.include_async);
assert!(cfg.include_streaming);
}
#[test]
fn test_no_empty_files_python() {
let gen = SdkGenerator::new();
let output = gen.generate_python(&default_config());
for file in &output.files {
assert!(
!file.content.is_empty(),
"Python file {} should not be empty",
file.path
);
}
}
#[test]
fn test_no_empty_files_typescript() {
let gen = SdkGenerator::new();
let output = gen.generate_typescript(&default_config());
for file in &output.files {
assert!(
!file.content.is_empty(),
"TypeScript file {} should not be empty",
file.path
);
}
}
}