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
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ install-hexgate: ## Ensure the agent-server venv is Python 3.13 with the hexgate
@$(AGENT_PY) -c 'import deepagents' 2>/dev/null || \
uv pip install --python demo/agent-server/.venv -e 'demo/agent-server[itsm]'

register: install-hexgate ## Register the healthcare + devops + itsm agents on the HexGate platform (reads HEXGATE_KEY from demo/agent-server/.env).
register: install-hexgate ## Register the healthcare + devops + itsm + hr agents on the HexGate platform (reads HEXGATE_KEY from demo/agent-server/.env).
@if [ -f demo/agent-server/.env ]; then set -a; . demo/agent-server/.env; set +a; fi; \
if [ -z "$$HEXGATE_KEY" ]; then \
echo "HEXGATE_KEY not set — add it to demo/agent-server/.env or export it."; exit 1; fi; \
Expand All @@ -65,7 +65,10 @@ register: install-hexgate ## Register the healthcare + devops + itsm agents on t
PYTHONPATH=$(AGENT_PATH) $(HEXGATE) register --agent agent_server.agents.devops_agent:agent && \
echo "→ registering itsm_agent" && \
PYTHONPATH=$(AGENT_PATH) $(HEXGATE) register --agent agent_server.agents.itsm_agent:agent \
--tools agent_server.agents.itsm_agent:TOOLS --model gpt-4o-mini
--tools agent_server.agents.itsm_agent:TOOLS --model gpt-4o-mini && \
echo "→ registering hr_agent" && \
PYTHONPATH=$(AGENT_PATH) $(HEXGATE) register --agent agent_server.agents.hr_agent:agent \
--tools agent_server.agents.hr_agent:TOOLS --model gpt-4o-mini

# -- test -------------------------------------------------------------------
test: ## Run the proxy test suite.
Expand Down
6 changes: 4 additions & 2 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,16 @@ Open <http://localhost:8873>.
1. Loading the app redirects you to **/login** (the route guard fires).
2. Log in as one of the demo accounts (all share the same password, `hexademo`):
- `guest@example.com` (no role) — exercises the fail-closed `default` (deny)
- `vince@hexamind.ai` (viewer), `olivia@hexamind.ai` (operator),
- `vince@hexamind.ai` (default), `olivia@hexamind.ai` (operator),
`aaron@hexamind.ai` (admin) — devops roles
- `nadia@clinic.org` (nurse), `priya@clinic.org` (physician),
`bianca@clinic.org` (billing_staff) — healthcare roles
- `alice@hexamind.ai` (requester), `carla@hexamind.ai` (implementer),
`emma@hexamind.ai` (change_manager), `gabriel@hexamind.ai` (cab_manager) — itsm roles
- `hugo@hexamind.ai` (default), `manon@hexamind.ai` (manager),
`chloe@hexamind.ai` (gestionnaire_rh) — hr roles

The `role` only matters for HexGate-gated agents (healthcare / devops / itsm
The `role` only matters for HexGate-gated agents (healthcare / devops / itsm / hr
with `HEXGATE_KEY` set), where it scopes the per-tool policy. The accounts come
from [`demo-users.yaml`](demo-users.yaml), upserted on
startup when `PLATFORM_DEMO_USERS_FILE` is set (the `make dev` launcher sets
Expand Down
46 changes: 33 additions & 13 deletions demo-users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,66 +36,86 @@ users:
# ── Hexamind org (DevOps + ITSM) ── one merged group; everyone can reach BOTH
# agents. Each keeps a single role, meaningful to one agent; talking to the
# other falls through to that policy's fail-closed `default` (deny).
# DevOps roles: viewer < operator < admin (devops_policy.yaml)
# DevOps roles: default < operator < admin (devops_policy.yaml)
# ITSM roles: requester, implementer, change_manager, cab_manager
# (itsm_policy.yaml — ownership/scope key off `name`, which
# must match the seed in itsm_db.py: Alice Martin owns
# CHG0001, Carla Robert is its implementer).
- email: vince@hexamind.ai
password: hexademo
name: Vince Reilly # devops: read service logs only
role: viewer
agents: [devops, itsm]
role: default
agents: [devops, itsm, hr]
- email: olivia@hexamind.ai
password: hexademo
name: Olivia Park # devops: restart/scale in dev & staging (replicas <= 10); prod walled off
role: operator
agents: [devops, itsm]
agents: [devops, itsm, hr]
- email: mark@hexamind.ai
password: hexademo
name: Mark Johnson
role: operator
agents: [devops, itsm]
agents: [devops, itsm, hr]
- email: aaron@hexamind.ai
password: hexademo
name: Aaron Mensah # devops: full ops incl. prod, higher cap, the only role that can delete
role: admin
agents: [devops, itsm]
agents: [devops, itsm, hr]

- email: alice@hexamind.ai
password: hexademo
name: Alice Martin # itsm: create + edit own drafts + submit (owns CHG0001)
role: requester
agents: [devops, itsm]
agents: [devops, itsm, hr]
- email: carla@hexamind.ai
password: hexademo
name: Carla Robert # itsm: read-only, scoped to changes they implement (CHG0001)
role: implementer
agents: [devops, itsm]
agents: [devops, itsm, hr]
- email: emma@hexamind.ai
password: hexademo
name: Emma Dubois # itsm: read all + edit assessment + authorize (Assess→Authorize)
role: change_manager
agents: [devops, itsm]
agents: [devops, itsm, hr]
- email: gabriel@hexamind.ai
password: hexademo
name: Gabriel Laurent # itsm: read all + schedule decision only (Authorize→Schedule)
role: cab_manager
agents: [devops, itsm]
agents: [devops, itsm, hr]

# ── Clinic org (Healthcare) ── healthcare agent only.
# roles: nurse, physician, billing_staff (healthcare_policy.yaml)
- email: nadia@clinic.org
password: hexademo
name: Nadia # clinical reads + order labs; cannot prescribe or share PHI
role: nurse
agents: [healthcare]
agents: [healthcare, hr]
- email: priya@clinic.org
password: hexademo
name: Priya # full clinical authority; controlled meds gated, PHI shares only to allowlisted domains
role: physician
agents: [healthcare]
agents: [healthcare, hr]
- email: bianca@clinic.org
password: hexademo
name: Bianca # billing/claims lookups only; clinical data walled off
role: billing_staff
agents: [healthcare]
agents: [healthcare, hr]

# ── HR agent ── roles: default < manager < gestionnaire_rh (hr_policy.yaml).
# One user per role; the role widens the readable employee fields and unlocks
# salary/medical/export/offboarding.
- email: hugo@hexamind.ai
password: hexademo
name: Hugo Bernard # annuaire + annuaire fields only
role: default
agents: [hr]
- email: manon@hexamind.ai
password: hexademo
name: Manon Lefevre # + leave_balance / performance_rating (team-scoped in a real impl; stubs gate by field only)
role: manager
agents: [hr]
- email: chloe@hexamind.ai
password: hexademo
name: Chloe Garnier # paie, médical, export (≤1000), offboarding
role: gestionnaire_rh
agents: [hr]
9 changes: 5 additions & 4 deletions demo/agent-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ llm = [
"google-adk>=1.0", # DevOps (google-adk runtime)
"litellm>=1.40", # DevOps model backend (openai/* via Google ADK)
]
# ITSM (deepagents/LangChain). Heavier stack, so kept out of `llm` — pulled in
# alongside hexgate by `make install-hexgate` (the ITSM agent is gated anyway).
# LangChain agents (ITSM + HR). Heavier stack, so kept out of `llm` — pulled in
# alongside hexgate by `make install-hexgate` (these agents are gated anyway).
itsm = [
"langchain-openai>=0.2", # ITSM model backend (ChatOpenAI)
"langchain-core>=0.3", # ITSM tools (@tool)
"langchain-openai>=0.2", # model backend (ChatOpenAI)
"langchain-core>=0.3", # tools (@tool)
"langgraph>=0.2.46", # HR agent (create_react_agent; `prompt=` kwarg lands in 0.2.46)
"deepagents>=0.0.5", # ITSM agent (create_deep_agent over LangGraph)
]
dev = [
Expand Down
9 changes: 8 additions & 1 deletion demo/agent-server/src/agent_server/agents/devops.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

logger = logging.getLogger("agent_server.devops")

# The elevated DevOps roles (devops_policy.yaml). Anything else — no role, or a
# role from another agent (nurse, requester…) — normalizes to the `default`
# baseline (read logs) rather than passing an unrecognized string to the policy.
_DEVOPS_ROLES = {"operator", "admin"}


class DevopsAgent:
framework = "google-adk"
Expand Down Expand Up @@ -45,7 +50,9 @@ async def run(
# user block.
caller = protocol.caller(context)
user_id = caller.get("id") or "hexui-demo"
role = caller.get("role") or os.getenv("HEXGATE_ROLE", "operator")
role = caller.get("role") or os.getenv("HEXGATE_ROLE", "default")
if role not in _DEVOPS_ROLES:
role = "default"
events = devops_agent.stream_as(text, user_id=user_id, role=role)
else:
events = devops_agent.stream(text)
Expand Down
64 changes: 64 additions & 0 deletions demo/agent-server/src/agent_server/agents/hr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""HexaUI contract wrapper for the HR (RH) agent.

Resolves the OpenAI key, picks the plain or HexGate-gated path, and projects each
LangChain event into a native event. The agent lives in ``hr_agent``; the caller's
``role`` (default < manager < gestionnaire_rh) flips each policy decision.
"""

from __future__ import annotations

import logging
import os
from collections.abc import AsyncIterator
from typing import Any

from .. import protocol

logger = logging.getLogger("agent_server.hr")

# The elevated HR roles (hr_policy.yaml). Anything else — no role, or a role from
# another agent (nurse, viewer, requester…) — normalizes to the `default`
# baseline rather than passing an unrecognized string to the policy.
_HR_ROLES = {"manager", "gestionnaire_rh"}


class HrAgent:
framework = "langchain"

async def run(
self, *, input: dict[str, Any], context: dict[str, Any]
) -> AsyncIterator[dict]:
# ChatOpenAI (the model backend) reads OPENAI_API_KEY from the env.
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
yield protocol.error(
"No OpenAI API key available. Set OPENAI_API_KEY in the "
"agent-server .env (or the process environment)."
)
return

# Lazy import so a missing langchain/hexgate install doesn't break the
# roster — only picking the HR agent pays the import cost.
from . import hr_agent

# HexGate-gated whenever configured; plain graph otherwise.
if os.getenv("HEXGATE_KEY"):
# `name` / `role` ride in `context.user` (CONTRACT.md §5); fall back to
# a static identity for standalone runs that send no user block.
caller = protocol.caller(context)
identity = caller.get("name") or "hexui-demo"
role = caller.get("role") or os.getenv("HEXGATE_ROLE", "default")
if role not in _HR_ROLES:
role = "default"
events = hr_agent.stream_as(input, user_id=identity, role=role)
else:
events = hr_agent.stream(input)

try:
async for event in events:
native_event = hr_agent.to_native_event(event)
if native_event is not None:
yield native_event
except Exception as exception: # noqa: BLE001 — degrade to a visible error event
logger.exception("hr run failed")
yield protocol.error(f"agent failed: {exception}")
Loading
Loading