Skip to content
Open
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ hex = "0.4"
rand = "0.8.5"
walkdir = "2.5.0"
url = "2.5.8"
rust-embed = "8"
rust-embed = { version = "8", features = ["include-exclude"] }
zip = "2.2"
flate2 = "1.1"
sha2 = "0.10"
Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,58 @@

💡 The main idea of `apx` is to provide convenient, fast and AI-friendly development experience.

## Agents

`apx init --addon agent` adds a complete agent framework — tool-calling loop, composition patterns, OBO auth, MCP, and dev UI.

```
┌─────────────────────────────────────────────────────────────┐
│ /_apx/agent (dev UI) POST /invocations │
│ │ │ │
│ └──────────┐ ┌────────────────┘ │
│ ▼ ▼ │
│ ┌───────────┐ ┌──────────────┐ │
│ │ LlmAgent │────▶│ FMAPI (LLM) │ │
│ └─────┬─────┘ └──────────────┘ │
│ │ tool calls │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌────────┐ ┌───────────┐ │
│ │ Local │ │ Genie │ │ Sub-agent │ │
│ │ tools │ │ Space │ │ /invoke │ │
│ └────┬────┘ └────┬───┘ └─────┬─────┘ │
│ │ │ │ │
│ └───────────┴───────────┘ │
│ OBO auth (X-Forwarded-Access-Token) │
│ forwarded through every call │
└─────────────────────────────────────────────────────────────┘
│ │
/mcp/sse /.well-known/agent.json
(MCP server) (A2A discovery)
```

**What you get out of the box:**

| Feature | How |
|---|---|
| OBO auth in every tool | `ws: Dependencies.UserClient` — token flows automatically |
| MCP server | `/mcp/sse` — connect Claude Desktop, Cursor, etc. |
| A2A discovery | `/.well-known/agent.json` — auto-populated at request time |
| Agent composition | `SequentialAgent`, `ParallelAgent`, `LoopAgent`, `RouterAgent`, `HandoffAgent` |
| Dev UI | `/_apx/agent` — chat, tool trace, MCP URL copy |
| Deploy | `apx deploy` — one command to production |
| MLflow eval | `app_predict_fn(url)` → `mlflow.genai.evaluate()` |

**Define tools as plain functions — type hints become the schema:**

```python
def query_genie(question: str, space_id: str, ws: Dependencies.Workspace) -> str:
"""Answer a question using a Genie Space."""
return ws.genie.ask(space_id=space_id, question=question).answer or ""

agent = Agent(tools=[query_genie])
```

## 🚀 Quickstart

Install `apx`:
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tokio.workspace = true
tracing.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
chrono.workspace = true
reqwest.workspace = true
toml.workspace = true
Expand Down
49 changes: 34 additions & 15 deletions crates/cli/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use crate::common::find_app_dir;
use crate::run_cli_async_helper;
use apx_core::api_generator::generate_openapi;
use apx_core::common::{
ensure_dir, format_elapsed_ms, run_command_streaming_with_output, run_preflight_checks, spinner,
ensure_dir, format_elapsed_ms, read_project_metadata, run_command_streaming_with_output,
run_preflight_checks, spinner,
};
use apx_core::dotenv::DotenvFile;
use apx_core::external::uv::Uv;

const DEFAULT_BUILD_DIR: &str = ".build";
Expand Down Expand Up @@ -40,39 +42,46 @@ pub async fn run(args: BuildArgs) -> i32 {
async fn run_inner(args: BuildArgs) -> Result<(), String> {
let app_path = find_app_dir(args.app_path)?;
let build_dir = app_path.join(&args.build_path);

println!("Building project in {}", app_path.display());
run_build(&app_path, &build_dir).await?;
println!("Build completed");
Ok(())
}

/// Core build logic shared with `apx deploy`.
pub async fn run_build(app_path: &Path, build_dir: &Path) -> Result<(), String> {
// Run preflight checks: generate _metadata.py, __dist__, uv sync, version file, bun install if needed
debug!("Running preflight checks before build");
let _preflight = run_preflight_checks(&app_path).await?;
let _preflight = run_preflight_checks(app_path).await?;

// Set up build directory
if build_dir.exists() {
fs::remove_dir_all(&build_dir)
fs::remove_dir_all(build_dir)
.map_err(|err| format!("Failed to remove build directory: {err}"))?;
}
ensure_dir(&build_dir)?;
ensure_dir(build_dir)?;
fs::write(build_dir.join(".gitignore"), "*\n")
.map_err(|err| format!("Failed to write build .gitignore: {err}"))?;

generate_openapi(&app_path).await?;

if args.skip_ui_build {
println!("Skipping UI build");
} else {
build_ui(&app_path).await?;
generate_openapi(app_path).await?;
let meta = read_project_metadata(app_path)?;
if meta.has_ui() {
build_ui(app_path).await?;
}

build_wheel(&app_path, &args.build_path).await?;
copy_app_config_files(&app_path, &build_dir)?;
// build_path is relative to app_path; find_wheel_file needs the resolved build_dir
let build_path = build_dir
.strip_prefix(app_path)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| build_dir.to_path_buf());
build_wheel(app_path, &build_path).await?;
copy_app_config_files(app_path, build_dir)?;

let wheel_file = find_wheel_file(&build_dir)?;
let wheel_file = find_wheel_file(build_dir)?;
let requirements_path = build_dir.join("requirements.txt");
fs::write(&requirements_path, format!("{wheel_file}\n"))
.map_err(|err| format!("Failed to write requirements.txt: {err}"))?;

println!("Build completed");
Ok(())
}

Expand All @@ -91,6 +100,16 @@ async fn build_wheel(app_path: &Path, build_path: &Path) -> Result<(), String> {
let mut cmd = uv.build_wheel_command(app_path, build_path).into_command();
cmd.env("UV_DYNAMIC_VERSIONING_BYPASS", build_version);

// Forward UV_* vars from the project .env so users can set e.g. UV_OFFLINE=1
// or UV_NATIVE_TLS=1 once in .env rather than prefixing every command.
if let Ok(dotenv) = DotenvFile::read(&app_path.join(".env")) {
for (key, value) in dotenv.get_vars() {
if key.starts_with("UV_") {
cmd.env(&key, &value);
}
}
}

let result =
run_command_streaming_with_output(cmd, &sp, "🐍 Wheel:", "Failed to build Python wheel")
.await;
Expand Down
Loading