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
21 changes: 21 additions & 0 deletions docs/agents/reference/agent-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ Names must match `^[a-zA-Z_][a-zA-Z0-9_-]*$`. Empty models represent inherited o
external-agent behavior. The complete constructor and serialization semantics are
maintained in [api-reference.md](../api-reference.md) and
`AgentConfigSerializer`; use those sources when adding a newly supported field.

## Jev agents

Use `JevAgent(name, model="jev-1.13", questions=questions)` or
`AgentDef(name=name, kind="jev", model="jev-1.13", questions=questions)`.
Questions are dictionaries with `instructions` and a `type`:
`choice` uses a `choices` map, `score` uses an ordered `scale`, and `boolean`
returns a probability. Omit questions to supply `context={"questions": questions}`.

Use `runtime.plan(agent, prompt)` to compile or `runtime.start(agent, prompt)`
to run. Call `handle.join()` and check `result.is_success` or `result.error`.
`result.output["result"]` preserves `model`, `answers`, `usage`, `latencyMs`
and optional `requestId`. Credentials and inference stay on Conductor.
No chat model or Python worker is needed.

[Example](../../../examples/agents/jev_agent.py)

For server-side Jev routing, use `Agent(strategy="router", router=selector,
agents=children)`. The Jev selector must have one fixed choice question whose
choice keys match the child agent names. No parent chat model is needed.
Routing runs one child and preserves its structured result.
2 changes: 2 additions & 0 deletions docs/agents/reference/agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"properties": {
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$" },
"model": { "type": ["string", "null"] },
"kind": { "const": "jev" },
"questions": { "type": "object", "additionalProperties": { "type": "object" } },
"baseUrl": { "type": ["string", "null"] },
"strategy": { "type": ["string", "null"] },
"maxTurns": { "type": ["integer", "null"], "minimum": 0 },
Expand Down
10 changes: 8 additions & 2 deletions examples/agentic_workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Agentic Workflow Examples

AI/LLM workflow examples using Conductor's built-in system tasks (`LLM_CHAT_COMPLETE`, `LLM_INDEX_TEXT`, `LLM_SEARCH_INDEX`, MCP tools) combined with Python workers.
AI workflow examples using Conductor's built-in system tasks, with Python workers where needed.

All examples use **inline ChatMessage objects** for system prompts -- no named prompt templates or AIOrchestrator required. They work with OSS Conductor with AI/LLM support.
Chat examples use inline ChatMessage objects for system prompts. No named prompt templates or AIOrchestrator required.

## Prerequisites

The AI decision example requires server-side `AI_DECISION` support and Jev credentials. It needs no chat model or Python worker. The other examples require:

- Conductor server with AI/LLM support running (e.g., `http://localhost:7001/api`)
- LLM provider named `openai` configured with a valid API key
- `export CONDUCTOR_SERVER_URL=http://localhost:7001/api`
Expand All @@ -14,6 +16,7 @@ All examples use **inline ChatMessage objects** for system prompts -- no named p

| Example | Description | Interactive? | Pattern |
|---------|-------------|:------------:|---------|
| [ai_decision_routing.py](ai_decision_routing.py) | Route requests and return the selected branch's result | No | AI_DECISION + SwitchTask + InlineTask |
| [llm_chat.py](llm_chat.py) | Automated multi-turn science Q&A between two LLMs | No | LoopTask + LLM_CHAT_COMPLETE + worker for history |
| [llm_chat_human_in_loop.py](llm_chat_human_in_loop.py) | Interactive chat with WAIT task pauses for user input | Yes | LoopTask + WaitTask + LLM_CHAT_COMPLETE |
| [multiagent_chat.py](multiagent_chat.py) | Multi-agent debate with moderator routing between panelists | No | LoopTask + SwitchTask + SetVariableTask + JavaScript routing |
Expand All @@ -23,6 +26,9 @@ All examples use **inline ChatMessage objects** for system prompts -- no named p
## Quick Start

```bash
# Jev request routing with server-managed credentials
CONDUCTOR_SERVER_URL=http://localhost:8080/api python -m examples.agentic_workflows.ai_decision_routing

# Automated multi-turn chat (no interaction needed)
python examples/agentic_workflows/llm_chat.py

Expand Down
85 changes: 85 additions & 0 deletions examples/agentic_workflows/ai_decision_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Route a request with AI_DECISION → SWITCH → INLINE branch → INLINE result.

Requires server-side AI_DECISION support and Jev credentials. No worker needed.
Run: CONDUCTOR_SERVER_URL=http://localhost:8080/api python -m examples.agentic_workflows.ai_decision_routing
"""

import argparse
import json
import time

from conductor.client.configuration.configuration import Configuration
from conductor.client.orkes_clients import OrkesClients
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
from conductor.client.workflow.task.ai_decision_task import AiDecisionTask
from conductor.client.workflow.task.inline import InlineTask
from conductor.client.workflow.task.switch_task import SwitchTask


def create_workflow(executor) -> ConductorWorkflow:
workflow = ConductorWorkflow(executor=executor, name="ai_decision_routing", version=1)
decision = AiDecisionTask(
task_ref_name="decision",
model="jev-1.13",
state=workflow.input("request"),
questions={
"route": {
"type": "choice",
"instructions": "Choose the team best suited to handle this request.",
"choices": {
"billing": "Payments, invoices, refunds, or subscriptions.",
"technical": "Errors, outages, or product troubleshooting.",
},
}
},
)
billing = InlineTask(
"handle_billing",
script='({team: "billing", nextAction: "review_invoice", '
'message: "Check invoice line items and payment records.", request: $.request})',
bindings={"request": workflow.input("request")},
)
technical = InlineTask(
"handle_technical",
script='({team: "technical", nextAction: "collect_diagnostics", '
'message: "Collect error logs and steps to reproduce.", request: $.request})',
bindings={"request": workflow.input("request")},
)
route = SwitchTask("route_request", decision.output("selectedCase"))
route.switch_case("billing", [billing])
route.switch_case("technical", [technical])
result = InlineTask(
"selected_result",
script="$.billing || $.technical",
bindings={"billing": billing.output("result"), "technical": technical.output("result")},
)
workflow >> decision >> route >> result
workflow.output_parameters({
"decision": decision.output(),
"result": result.output("result"),
})
return workflow


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--request", default="I was charged twice on my latest invoice.")
args = parser.parse_args()
clients = OrkesClients(configuration=Configuration())
workflow = create_workflow(clients.get_workflow_executor())
workflow.register(overwrite=True)
workflow_id = workflow.start_workflow_with_input({"request": args.request})
print(f"Workflow: {workflow_id}")
client = clients.get_workflow_client()
while True:
result = client.get_workflow(workflow_id=workflow_id, include_tasks=False)
if result.is_completed():
break
time.sleep(1)
if result.status != "COMPLETED":
raise SystemExit(f"{result.status}: {result.reason_for_incompletion}")
print(json.dumps(result.output, indent=2))


if __name__ == "__main__":
main()
8 changes: 8 additions & 0 deletions examples/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ the Conductor CLI manages the server with `conductor server start`.
Framework-specific examples are in [ADK](adk/README.md),
[LangGraph](langgraph/README.md), and [OpenAI Agents SDK](openai/README.md).
Review tool side effects before using real credentials.

Jev: [jev_agent.py](jev_agent.py) compiles by default. Pass `--run` for inference.

- `jev_nested_triage.py`: Jev department selection, Jev specialist selection, then a Jev specialist.
- `luna_jev_triage.py --model INTEGRATION/luna-6`: Luna selects one of ten Jev specialists.

Both compile by default. Pass `--run` for inference. These require the server's
Jev router support and structured output for single-turn routers without synthesis.
53 changes: 53 additions & 0 deletions examples/agents/jev_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Compile a Jev agent. Pass --run for inference. Configure credentials on Conductor."""

import argparse
import json
import os

from conductor.ai.agents import AgentRuntime, JevAgent
from conductor.client.configuration.configuration import Configuration

PROMPT = "The customer reports a duplicate charge on the latest invoice."


def support_agent():
return JevAgent(
name="jev_support_agent",
model="jev-1.13",
questions={
"department": {
"type": "choice",
"instructions": "Which team should handle this issue?",
"choices": {
"billing": "Payment and invoice issues",
"technical": "Bugs and software issues",
"other": "Other requests",
},
}
},
)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run", action="store_true", help="Start live Jev inference")
args = parser.parse_args()
config = Configuration(
server_api_url=os.environ.get("CONDUCTOR_SERVER_URL", "http://localhost:8080/api")
)
with AgentRuntime(config) as runtime:
agent = support_agent()
if not args.run:
print(json.dumps(runtime.plan(agent, PROMPT), indent=2))
return

handle = runtime.start(agent, PROMPT)
print("Execution:", handle.execution_id)
result = handle.join(timeout=120)
if not result.is_success:
raise RuntimeError(f"{result.status}: {result.error}")
print(json.dumps(result.output["result"], indent=2))


if __name__ == "__main__":
main()
86 changes: 86 additions & 0 deletions examples/agents/jev_nested_triage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Three levels: Jev department selector, Jev specialist selector, Jev specialist.

Requires server support for Jev routers. Compile by default, --run for inference.
"""

import argparse
import json
import os

from conductor.ai.agents import Agent, AgentRuntime, JevAgent, Strategy
from conductor.client.configuration.configuration import Configuration
from jev_specialists import SPECIALTIES, specialists

PROMPT = "Our latest invoice has two settled charges with different transaction IDs for the same purchase."


def routing_team(name, children, descriptions):
return Agent(
name=name,
strategy=Strategy.ROUTER,
router=JevAgent(
name=f"{name}_selector",
model="jev-1.13",
questions={
"agent": {
"type": "choice",
"instructions": "Choose the agent best suited to handle this request.",
"choices": descriptions,
}
},
),
agents=children,
max_turns=1,
synthesize=False,
)


def triage_agent():
leaves = specialists()
departments = []
for department in ("billing", "technical", "account"):
members = {
name: agent for name, agent in leaves.items() if SPECIALTIES[name][0] == department
}
departments.append(
routing_team(
f"jev_{department}_team",
list(members.values()),
{agent.name: SPECIALTIES[name][1] for name, agent in members.items()},
)
)
return routing_team(
"jev_nested_triage",
departments,
{
"jev_billing_team": "Charges, refunds and subscriptions",
"jev_technical_team": "API errors, outages, integrations and setup",
"jev_account_team": "Access, security and privacy",
},
)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run", action="store_true")
parser.add_argument("--prompt", default=PROMPT)
args = parser.parse_args()
config = Configuration(
server_api_url=os.getenv("CONDUCTOR_SERVER_URL", "http://localhost:8080/api")
)
with AgentRuntime(config) as runtime:
agent = triage_agent()
if not args.run:
print(json.dumps(runtime.plan(agent, args.prompt), indent=2))
return
runtime.deploy(agent)
handle = runtime.start(agent, args.prompt)
print("Execution:", handle.execution_id)
result = handle.join(timeout=180)
if not result.is_success:
raise RuntimeError(result.error)
print(json.dumps(result.output, indent=2))


if __name__ == "__main__":
main()
Loading
Loading