Hikigai

AppSDK

Application SDK

Lightweight SDK for invoking healthcare AI agents in your medical applications and EHR systems.HIPAA-ready, fast, and production-ready with streaming responses for clinical workflows and session support for patient consultations.

v0.2.5Python 3.10+StreamingSessionsWebSocket

Installation

Using pip

pip install hikigai-appsdk

Quick Start

import asyncio
from hikigai.appsdk import AppClient

# Initialize client (or set HIKIGAI_API_KEY / HIKIGAI_PROJECT_ID)
client = AppClient(
    api_key="your-api-key",
    project_id="your-project-id",
)

# Get an agent by slug or id
agent = client.agent("medical-coder")

# Invoke
response = agent.invoke("Patient presents with fever and cough...")
print(response.content)

# Stream
for chunk in agent.stream("Tell me about diabetes"):
    print(chunk, end="", flush=True)

# Session-based conversations
session_agent = agent.with_session("user-123")
session_agent.invoke("What is hypertension?")
session_agent.invoke("How is it treated?")  # Remembers context

# Live WebSocket (install: pip install websockets)
async def live():
    async with client.live(agent.slug, user_id="app-user") as session:
        await session.send_text("Hello")
        async for event in session:
            print(event)
            if event.get("type") in ("turn_complete", "end"):
                break

asyncio.run(live())

Client Reference

class AppClient

AppClientCore

Main client for invoking AI agents. Manages authentication, HTTP connections, and provides access to deployed agents.

Constructor

client = AppClient(
    api_key: Optional[str] = None,        # API key (or HIKIGAI_API_KEY env var)
    project_id: Optional[str] = None,     # Project ID (or HIKIGAI_PROJECT_ID env var)
    base_url: Optional[str] = None,       # API endpoint (defaults to production)
    timeout: float = 30.0,                # Request timeout in seconds
    sona_url: Optional[str] = None,       # Optional SONA service URL
    sona_api_key: Optional[str] = None,   # Optional SONA API key
)

Methods

MethodReturnsDescription
agent(agent_id: str)RuntimeAgentGet agent by ID or slug for invocation
list_agents(category=None, tags=None, search=None)List[RuntimeAgent]List available agents with optional filters
invoke(agent_id, input, session_id=None, provider=None, model=None, connectors=None, plugin_context=None)InvokeResponseInvoke an agent without fetching RuntimeAgent first
live(agent_id, session_id=None, user_id='sdk-user', context=None, connectors=None)StreamSessionOpen a live WebSocket session (optional MCP connectors)
create_session(external_user_ref=None, ttl_seconds=None, app_id='_', metadata=None)SessionContextCreate a server-side session context store
session(session_id, app_id='_')SessionContextAttach to an existing session context
list_sessions(user_ref=None, limit=50, cursor=None)DictList session contexts
get_auth_token()Dict[str, Any]Exchange API key for a short-lived access token

Properties

identity()IdentityClientEnd-user auth (Cognito, MFA, QR, SSO)
sona()SONAClientSONA personalization (edits, preferences, suggestions)
storage()StorageClientApp object storage

Context Manager

with AppClient() as client:
    agent = client.agent("my-agent")
    response = agent.invoke("Hello")
# Resources automatically released

Agent Reference

class RuntimeAgent

RuntimeAgentModel

Represents a deployed agent ready for invocation. Provides methods for synchronous invocation, streaming, and session management.

Properties

PropertyTypeDescription
idstrUnique agent ID
namestrAgent name
slugstrURL-friendly slug
versionstrAgent version
descriptionstr | NoneAgent description
statusstrDeployment status ('active', 'pending', etc.)
endpointstr | NoneInvocation endpoint URL

Methods

MethodReturnsDescription
invoke(input, session_id=None, provider=None, model=None, timeout=None, connectors=None, plugin_context=None)InvokeResponseInvoke agent synchronously with optional session, MCP connectors, and plugins
stream(input, session_id=None, provider=None, model=None, connectors=None)Iterator[str]Stream agent response chunks in real-time (SSE)
with_session(session_id: str)RuntimeAgentCreate agent instance with persistent session
live_session(session_id=None, *, connectors=None, context=None, user_id='sdk-user')StreamSessionOpen a bidirectional WebSocket live session (optional MCP connectors)

Basic Usage

agent = client.agent("medical-coder")

# Synchronous invocation
response = agent.invoke("Patient has diabetes...")
print(response.content)

# With MCP connectors + SONA plugin context
response = agent.invoke(
    "Prepare patient chart",
    connectors={"epic-ehr": {"headers": {"Authorization": "Bearer ..."}}},
    plugin_context={"sona": {"user_id": "dr-smith-uuid"}},
)

# Streaming
for chunk in agent.stream("Explain hypertension"):
    print(chunk, end="")

# With session
response = agent.invoke("My name is Alice", session_id="user-123")
response = agent.invoke("What's my name?", session_id="user-123")  # Remembers "Alice"

Workflow agents (including those that reuse marketplace leaves via AgentSDK source="marketplace") are invoked the same way — use the root agent's slug or id.

Response Types

class InvokeResponse

InvokeResponsePydantic Model

Response from a synchronous agent invocation containing output content and metadata.

PropertyTypeDescription
content*strAgent output content
agent_idstrID of the agent that responded
agent_versionstr | NoneVersion of the agent
session_idstr | NoneSession ID if used
statusstrStatus (success, requires_human_review, etc.)
outputDict | NoneStructured agent output when available
confidenceClinicalConfidence | NoneClinical confidence score (0–1)
safety_flagsList[SafetyFlag]Safety flags (severity: low|medium|high|critical)
citationsList[ClinicalCitation]Evidence-based clinical citations
metadataInvocationMetadataInvocation metadata (latency, tokens, etc.)
messagestr | NoneOptional warning or info message
pluginsDict | NonePlugin metadata (e.g. plugins['sona'])

Example

response = agent.invoke("Hello")

print(response.content)                    # "Hi! How can I help you?"
print(response.status)                     # "success"
print(response.metadata.latency_ms)        # 234
print(response.metadata.tokens_used)       # 156
print(response.metadata.tools_called)      # ["web_search"]
print(response.plugins)                    # {"sona": {"output_id": "..."}}

class InvocationMetadata

InvocationMetadataPydantic Model

Metadata about an agent invocation including performance metrics and execution details.

invocation_idstrUnique invocation ID
latency_msint | NoneResponse latency in milliseconds
timestampdatetimeInvocation timestamp
statusstrStatus ('success', 'error', etc.)
tokens_usedint | NoneTotal tokens consumed
tools_calledList[str]Tools used (from top-level gateway tools_called — includes MCP CallTool on Sequential/Parallel/Loop leaves)
phi_redactedboolWhether PHI was redacted
trace_idstr | NoneTrace ID for troubleshooting

MCP on Workflow Agents

Deploy a Sequential / Parallel / Loop agent with root connectors in AgentSDK → MCP on Sub-Agents. Then pass the same connector map on invoke or live. MCP tools attach to every LlmAgent leaf. Check metadata.tools_called (populated from the gateway top-level field) to confirm CallTool ran.

from hikigai.appsdk import AppClient

client = AppClient(api_key="...", project_id="...")
agent = client.agent("vitals-fetch-subagent")

# Same connectors shape for simple LLM roots and Sequential leaves
response = agent.invoke(
    {"site_id": 1, "patient_id": "PATIENT001"},
    connectors={"idexx-mcp-server": {}},
    timeout=120,
)

print(response.content)                      # summary text / {"response": "..."}
print(response.metadata.tools_called)        # e.g. ["get_vitals_for_today"]
print(response.metadata.latency_ms)

# Live WebSocket — MCP on leaves too
# async with client.live(
#     "vitals-fetch-subagent",
#     connectors={"idexx-mcp-server": {}},
# ) as session:
#     await session.start(modality="text")
#     await session.send_text("Fetch today's vitals")
#     async for event in session:
#         if event.get("type") == "tool_call":
#             print(event)  # leaf CallTool frames
Note: Plain-text Sequential answers may be wrapped as {"response": "..."} so schema retries do not re-fire MCP. Empty tools_called with MCP server logs showing CallTool usually means an outdated AppSDK — upgrade and re-check top-level gateway fields.

Live WebSocket Sessions

Deploy first with AgentSDK. For Sequential / Parallel / Loop live agents, configure leaves with live_handoff when deploying (see AgentSDK → Workflow Agents). To reuse a marketplace agent as a leaf, deploy with source="marketplace" + name (documented under SubAgentConfig). AppSDK only opens the session — it does not define workflow structure.

Bidirectional realtime via StreamSession. Flow: auth/exchange → POST …/live/session → WebSocket at the returned wss_url (…/live) → binary PCM (protocol v2). Captions use source: input_transcription; replace pending text on partials. Requires the optional websockets dependency. Full protocol: API Reference → Live Sessions.

import asyncio
from hikigai.appsdk import AppClient

async def main():
    client = AppClient(api_key="...", project_id="...")
    client.get_auth_token()  # POST /auth/exchange — cache for mint
    # AppSDK generates session_id if omitted; always mints with {session_id, user_id}

    async with client.live("YOUR_AGENT_SLUG", user_id="app-transcriber") as session:
        await session.start(modality="audio", mime_type="audio/pcm;rate=16000")
        await session.send_data(pcm_bytes)  # raw bytes → binary frames

        async for event in session:
            if event.get("source") == "input_transcription":
                print(event["content"])  # replace UI on partials
            if event.get("type") == "end":
                break

        await session.end()

asyncio.run(main())

Attach MCP tools on live the same way as invoke: pass connectors. Mint then uses the gateway /live path so connectors resolve before bidirectional run_live.

async with client.live(
    "clinical-live-agent",
    user_id="app-user",
    connectors={"epic-ehr": {}},  # same shape as invoke
) as session:
    await session.send_text("Look up the patient chart")
    async for event in session:
        if event.get("type") == "tool_call":
            print(event)
        if event.get("type") in ("turn_complete", "end"):
            break

Speech: Translate & Transcribe

Real-time speech-to-speech translation and speech-to-text transcription via client.speech. These are not agents — nothing to deploy, and no agent slug. Available to team organizations with GCP connected, billed to your own cloud project. Full protocol: API Reference → Speech.

Translate

import asyncio
from hikigai.appsdk import AppClient

async def main():
    client = AppClient(api_key="hikigai_...", project_id="...")

    session = await client.speech.translate(
        target_language="es",
        echo_target_language=True,   # default: echo when input is already Spanish
        input_transcription=True,    # default
        output_transcription=True,   # default
    )

    async def send():
        # Any source format — converted to 16 kHz mono PCM for you.
        async for chunk in phone_call():
            await session.send_audio(chunk, encoding="mulaw", sample_rate=8000)

    asyncio.create_task(send())

    async for event in session:
        if event.type == "audio":
            speaker.write(event.audio)          # 24 kHz PCM
        elif event.type == "transcript":
            print(event.source, event.text, event.language_code)     # "input" / "output"

asyncio.run(main())

Transcribe

session = await client.speech.transcribe(
    languages=["en-US", "es-ES"],               # optional; omit to auto-detect
    custom_vocabulary=["Hikigai", "Kubernetes"],
)

await session.send_audio(pcm_16k)               # already 16 kHz mono PCM

async for event in session:
    if event.type == "transcript":
        print(event.text, event.language_code, "final" if event.final else "interim")

Note the code shapes differ: translate takes bare subtags (es), transcribe takes full locales (es-ES). Google caps transcription at 10 minutes; the gateway rotates the upstream transparently, so long sessions need no handling.

Changing language mid-conversation

Returns once the new language is live. A replacement model session is brought up before the old one is retired, so audio keeps flowing and the listener hears no gap.

await session.set_language("fr")   # no reconnect, no new token

Audio helpers

send_audio converts for you, but the helpers are exported for callers doing their own buffering.

from hikigai.appsdk import to_model_audio, chunk_audio, pcm_to_wav

pcm = to_model_audio(raw, encoding="mulaw", sample_rate=8000)  # telephony → 16 kHz
pcm = to_model_audio(raw, encoding="f32", sample_rate=48000)   # browser Float32
pcm = to_model_audio(raw, sample_rate=48000, channels=2)       # stereo → mono

for chunk in chunk_audio(pcm):        # ~100 ms frames, never splits a sample
    await session.send_audio(chunk)

open("out.wav", "wb").write(pcm_to_wav(translated))  # 24 kHz by default

Browser and mobile clients

Mint a short-lived token server-side so your API key never reaches the client.

token = client.speech.mint_token(
    "translate",
    target_language_code="ja",
    locked=True,   # pin the config — client cannot call configure
)
# Hand token["speech_token"] to the browser; it connects directly.
Try it first: the Voice Lab tab in your project console runs both endpoints from the browser and generates this code from whatever config you settle on.

Session Context Store

Server-side key/value context for apps (SessionContext), separate from conversation session_id memory on invoke.

# Create a context store
ctx = client.create_session(external_user_ref="dr-smith", ttl_seconds=3600)
ctx.set("patient_id", "MRN-12345")
ctx.set("visit_id", "V-99", if_match=None)

print(ctx.get("patient_id"))
print(ctx.get_all())
print(ctx.metadata())

# Attach later
ctx = client.session(ctx.session_id)
ctx.update({"chief_complaint": "chest pain"})
ctx.touch(ttl_seconds=7200)
ctx.delete("visit_id")
ctx.clear()

Session Management

Maintain conversation context across multiple invocations using sessions. Sessions enable multi-turn conversations where the agent remembers previous interactions.

Per-Invocation Sessions

Pass a session_id to each invocation to maintain context.

agent = client.agent("chatbot")

# First message
response1 = agent.invoke(
    "My name is Alice",
    session_id="user-123"
)

# Follow-up - agent remembers the name
response2 = agent.invoke(
    "What's my name?",
    session_id="user-123"
)
print(response2.content)  # "Your name is Alice"

Persistent Session Agent

Create a session-bound agent using with_session(). All invocations automatically use the same session.

agent = client.agent("assistant")

# Create session-bound agent
session_agent = agent.with_session("user-456")

# All invocations share the same session
session_agent.invoke("I live in New York")
session_agent.invoke("What's the weather like here?")  # Knows "here" = NYC

# Streaming works too
for chunk in session_agent.stream("Tell me more about my city"):
    print(chunk, end="")

Multiple Users

Maintain separate contexts for different users with different session IDs.

agent = client.agent("support-bot")

# User 1's conversation
user1 = agent.with_session("user-001")
user1.invoke("I'm having trouble logging in")

# User 2's isolated conversation
user2 = agent.with_session("user-002")
user2.invoke("How do I reset my password?")

# Contexts are completely separate
# User 1's issues don't affect User 2's conversation

Rooms

Create and use multi-party realtime rooms with RoomSession — same class as AgentSDK. Call join(name); the first join creates the room in your project. Then publish, listen for frames, and optionally negotiate WebRTC with the rtc_* helpers. Standalone (not on AppClient). Requires websockets.

Full method list and frame types: AgentSDK → RoomSession. Protocol details: API reference → Rooms.

Create, join, publish

import asyncio
from hikigai.appsdk import RoomSession

async def main():
    room = RoomSession(
        base_url="https://backend.hikigaiplatform.io",
        api_key="your-api-key",
        project_id="your-project-id",
    )
    async with room:
        await room.join("care-team-7")  # creates the room if new
        await room.publish("care-team-7", "note", {"text": "Labs ready"})
        await room.who("care-team-7")
        async for frame in room:
            if frame.get("type") == "message":
                print(frame["event"], frame["data"])
            elif frame.get("type") == "error":
                print(frame.get("code"), frame.get("message"))
                break

asyncio.run(main())

Auth

Pass exactly one of api_key, access_token, or room_token, plus project_id when using a key/JWT. End-user tokens are scoped to specific rooms and capabilities; project keys get full access in that project.

WebRTC signalling

await room.join("call-1")
await room.rtc_join("call-1")
async for frame in room:
    if frame["type"] == "rtc_peer_joined" and frame.get("initiator"):
        await room.rtc_offer("call-1", frame["id"], await make_offer())
    elif frame["type"] == "rtc_offer":
        await room.rtc_answer("call-1", frame["sender"], await answer(frame["sdp"]))

Even Realities G2 Glasses Integration

Build apps for Even Realities G2 smart glasses with a Python proxy that keeps your API key private. G2 apps run in the Even phone app WebView and drive the 640×350 green monochrome display via @evenrealities/even_hub_sdk. The glasses UI is always TypeScript; Python (or raw HTTP) only replaces the proxy. Full WebView / event loop: Node.js AppSDK G2 Guide. Console HTTP APIs: API Reference → Even G2. Starter (Node UI + Express): templates/even-g2.

Environment variables

Proxy needs HIKIGAI_API_KEY, HIKIGAI_PROJECT_ID, and HIKIGAI_AGENT_ID. On Hikigai deploy, key and project id are injected; set the agent id under Environment in the console. Optional: HIKIGAI_PLATFORM_URL, GLASSES_PAGE_CHARS (starter default 420).

app.json & voice notes

  • name ≤ 20, tagline ≤ 50; permissions typically network + g2-microphone.
  • Voice requires an agent with audio in its input modalities; list/text questions work with any text agent.
  • Send WAV via attachments (mime_type="audio/wav"), not an input_audio content array.

SDK helpers (pure, no network)

from hikigai.appsdk import (
    validate_even_g2_manifest,  # same app.json rules as the deploy pipeline
    paginate_for_glasses,       # split agent answers into display-sized pages
    normalize_for_glasses,      # strip markdown decorations agents emit
    even_g2_dev_mode_url,       # validate the live URL used in the dev-mode QR
    EVEN_G2_DISPLAY,            # {"width": 640, "height": 350}
    EVEN_G2_MAX_TEXT_CHARS,     # 2000 (official text-heavy template cap)
)

check = validate_even_g2_manifest(json.load(open("app.json")))
if not check.valid:
    raise SystemExit(check.error)

pages = paginate_for_glasses(response.content, max_chars=420)
qr_payload = even_g2_dev_mode_url(live_url)

1. Proxy API with Pagination

The proxy receives queries from the WebView, invokes the agent, and returns pages sized for the G2 display.

import os
from fastapi import FastAPI
from pydantic import BaseModel
from hikigai.appsdk import AppClient, normalize_for_glasses, paginate_for_glasses

app = FastAPI()
client = AppClient()  # reads HIKIGAI_API_KEY / HIKIGAI_PROJECT_ID
PAGE_CHARS = int(os.environ.get("GLASSES_PAGE_CHARS", "420"))
AGENT_ID = os.environ["HIKIGAI_AGENT_ID"]

class AskRequest(BaseModel):
    question: str
    sessionId: str | None = None

@app.post("/api/ask")
async def ask(req: AskRequest):
    agent = client.agent(AGENT_ID)
    response = agent.invoke(req.question, session_id=req.sessionId)

    clean = normalize_for_glasses(response.content)
    return {
        "answer": response.content,
        "pages": paginate_for_glasses(clean, max_chars=PAGE_CHARS),
        "sessionId": response.session_id,
    }

2. Voice Queries (WAV attachments)

Forward base64 WAV from the glasses mic via attachments. The agent must allow audio in its input modalities — text-only agents reject voice. List/preset text questions work with any text agent.

class AudioRequest(BaseModel):
    audio_base64: str
    mime_type: str | None = "audio/wav"
    sessionId: str | None = None

@app.post("/api/ask-audio")
async def ask_audio(req: AudioRequest):
    agent = client.agent(AGENT_ID)
    response = agent.invoke(
        "Answer the question in this recording.",
        session_id=req.sessionId,
        attachments=[{
            "data_base64": req.audio_base64,
            "mime_type": req.mime_type or "audio/wav",
        }],
    )

    clean = normalize_for_glasses(response.content)
    return {
        "pages": paginate_for_glasses(clean, max_chars=PAGE_CHARS),
        "sessionId": response.session_id,
    }

Multi-Cloud Deployment

client.cloud exposes the deployment catalog and BYOC credentials. Apps, agents, and MCP servers deploy to GCP or AWS targets from the same catalog.

from hikigai.appsdk import AppClient

client = AppClient(api_key=..., project_id=...)

catalog = client.cloud.catalog(workload="app")
for service in catalog.services:
    print(service.id, service.display_name, service.regions[0].id)

result = client.cloud.validate_target(
    workload="app",
    service_id="gcp-cloud-run",
    region="europe-west1",
    config={"cpu": "2", "memory": "1Gi"},
)

Events & Webhooks

client.events is the SDK surface for the platform event bus — HMAC-signed webhooks (at-least-once) andevents.stream() (at-most-once).

sub = client.events.create_webhook(
    url="https://ci.example.com/hooks/hikigai",
    event_types=["agent.deployed", "agent.deleted"],
    description="deploy notifications",
)
print(sub.secret)  # shown once — store it

# Live stream
async with client.events.stream(patterns=["job.*", "agent.deployed"]) as stream:
    async for event in stream:
        print(event.type, event.data)

Storage

Access via client.storage. Platform-managed object storage scoped to project + app_id — PDFs, images, audio, and other app outputs. Same client as AgentSDK.

Methods

MethodReturnsDescription
upload(app_id, file, filename, content_type=..., metadata=None, ttl=None)StorageUploadResultMultipart upload; optional metadata + object TTL. Includes signed_url on success.
signed_url(app_id, object_id, ttl=3600)SignedUrlResultMint a temporary download URL
list(app_id, content_type=None, status='active', limit=20, page=1)StorageListResultPaginated list with filters
get(app_id, object_id)StorageObjectInfoFetch object metadata
delete(app_id, object_id)NoneDelete an object
with open("audio.wav", "rb") as f:
    uploaded = client.storage.upload(
        app_id="app_123",
        file=f,
        filename="audio.wav",
        content_type="audio/wav",
        metadata={"session": "s-9"},
        ttl=86400,
    )

url = client.storage.signed_url("app_123", uploaded.object_id, ttl=3600)
print(url.signed_url, url.expires_at)

info = client.storage.get("app_123", uploaded.object_id)
print(info.original_filename, info.size_bytes, info.metadata)

page = client.storage.list("app_123", content_type="audio/wav", status="active", limit=20, page=1)
for obj in page.objects:
    print(obj.object_id, obj.original_filename)
print(page.total, page.total_pages)

client.storage.delete("app_123", uploaded.object_id)

SONA Personalization

Access via client.sona. Invoke with plugin_context={"sona": {"user_id": "..."}}to receive SONA metadata on response.plugins.

response = agent.invoke(
    "Draft discharge summary",
    plugin_context={"sona": {"user_id": "dr-smith-uuid"}},
)
output_id = response.plugins["sona"]["output_id"]

client.sona.submit_edit(output_id, "User-edited note text...")
client.sona.approve_output(output_id, user_id="dr-smith-uuid")
prefs = client.sona.get_preferences("dr-smith-uuid", agent_id=agent.id)
suggestions = client.sona.get_suggestions("dr-smith-uuid")

End-User Identity

Add signup, login, MFA, QR badge login, and SSO for your app's end users — clinicians, patients, staff. Each app is backed by a dedicated AWS Cognito user pool the platform provisions for you; login returns standard OIDC tokens. Everything is reached through client.identity.

class IdentityClient

client.identityCore

End-user authentication and account management for the app's Cognito pool, plus QR/PIN badge credentials. Access it as a property on AppClient.

Account & auth methods

MethodReturnsDescription
signup(app_id, email, password, first_name=None, last_name=None, role=None, attributes=None, metadata=None)dictRegister a new end user. attributes = Cognito fields (e.g. phone_number); metadata = opaque JSON as custom:metadata.
confirm(app_id, email, code)dictConfirm a signup with the emailed code
login(app_id, email, password)dictAuthenticate — returns OIDC tokens or an MFA challenge
refresh(app_id, refresh_token)dictExchange a refresh token for fresh tokens
logout(app_id, email)dictGlobally sign the user out
forgot_password(app_id, email)dictRequest a password-reset code (emailed by Cognito)
reset_password(app_id, email, code, new_password)dictSet a new password with the emailed code
change_password(app_id, access_token, current_password, new_password)dictChange password for a logged-in user
create_user(app_id, email, password, first_name, last_name, role=None, metadata=None)dictSame as signup — creates the Cognito/GCP user
get_user(app_id, email)dictFetch Cognito profile (requires app_id)
list_users(app_id, limit=50, offset=0)list[dict]List users in the app's pool
update_user(app_id, email, **fields)dictUpdate Cognito profile (role, name, metadata, is_active, phone_number). Email cannot be changed.
list_app_users(app_id, limit=50, offset=0)list[dict]List the app's end users
delete_app_user(app_id, email)NoneDelete a user from the pool
verify_qr(app_id, qr_payload)VerifiedUserVerify an HKG2 QR badge against the app pool
verify_pin(app_id, email, pin)VerifiedUserVerify PIN stored on Cognito
verify_sso(app_id, token)VerifiedUserVerify SSO id token for the app pool
issue_qr(app_id, email)dictIssue Cognito QR badge (same as issue_qr_login)
issue_pin(app_id, email, pin)dictSet PIN on the Cognito user

MFA, QR & SSO

MethodReturnsDescription
mfa_associate(app_id, access_token)dictBegin TOTP enrollment — returns secret for a QR code
mfa_verify(app_id, access_token, code, device_name=None)dictVerify the first code to finish enrollment
mfa_set_preference(app_id, access_token, enabled=True)dictTurn TOTP MFA on or off
mfa_respond(app_id, email, session, code)dictComplete a login that returned an MFA challenge
issue_qr_login(app_id, email)dictIssue a QR badge credential for a user
qr_login(app_id, qr_payload)dictExchange a scanned QR payload for tokens
configure_sso(app_id, provider_name, provider_type, …)dictAdd a Google/Okta/SAML/OIDC provider
sso_authorize_url(app_id, redirect_uri, provider=None)dictBuild the hosted-UI authorize URL
sso_exchange(app_id, code, redirect_uri)dictExchange the callback code for tokens

Setup (one-time, per app)

enable_identity(app_id, mode='managed', enabled_methods=…)dictProvision the app's Cognito pool
get_identity_config(app_id)dictGet config + provisioning status
byo_setup()dictCloudFormation params for bring-your-own Cognito
byo_test(role_arn, region=None)dictVerify a cross-account role before enabling BYO

Sign up, confirm, log in

attributes are Cognito profile fields (string values — e.g. phone_number in E.164). metadata is opaque app JSON stored as Cognito custom:metadata (GCP BYO uses custom claims). They are not interchangeable. Cognito/GCP is the source of truth — there is no separate EndUser DB index for profile data.

from hikigai.appsdk import AppClient

client = AppClient(api_key="hikigai_...", project_id="proj_123")

# 1. Register the end user
client.identity.signup(
    app_id="app_123",
    email="clinician@hospital.com",
    password="Temp#Pass2026",
    first_name="Asha",
    last_name="Mehta",
    role="clinician",
    # attributes → Cognito fields (strings only; e.g. phone_number in E.164)
    attributes={"phone_number": "+14155552671"},
    # metadata → Cognito custom:metadata JSON
    metadata={"doctorID": "6278383837"},
)

# 2. Confirm (skip if the pool auto-confirms)
client.identity.confirm("app_123", "clinician@hospital.com", code="123456")

# 3. Log in -> OIDC tokens
tokens = client.identity.login("app_123", "clinician@hospital.com", "Temp#Pass2026")
print(tokens["access_token"])

# 4. Refresh later
fresh = client.identity.refresh("app_123", tokens["refresh_token"])

Forgot password

# 1. Request reset code (Cognito emails it)
client.identity.forgot_password("app_123", "clinician@hospital.com")

# 2. User enters code + new password
client.identity.reset_password(
    "app_123", "clinician@hospital.com", code="123456", new_password="NewTemp#Pass2026"
)

Change password (logged in)

tokens = client.identity.login("app_123", "clinician@hospital.com", "Temp#Pass2026")
client.identity.change_password(
    "app_123",
    access_token=tokens["access_token"],
    current_password="Temp#Pass2026",
    new_password="NewTemp#Pass2026",
)

Update role / profile

# app_id + email required (Cognito is source of truth)
client.identity.update_user(
    "app_123", "clinician@hospital.com",
    role="doctor", first_name="Asha", last_name="Mehta",
)

Multi-factor login

# Login may return an MFA challenge instead of tokens
result = client.identity.login("app_123", "clinician@hospital.com", "Temp#Pass2026")

if result.get("status") == "challenge":
    code = input("Enter your authenticator code: ")
    tokens = client.identity.mfa_respond(
        app_id="app_123",
        email="clinician@hospital.com",
        session=result["session"],
        code=code,
    )
    print(tokens["access_token"])

QR badge login

# Admin: issue a badge for the user (HKG2 payload shown once)
badge = client.identity.issue_qr_login("app_123", "clinician@hospital.com")
print(badge["qr_payload"])   # encode into the printed badge

# Kiosk: scan the badge, exchange it for tokens
tokens = client.identity.qr_login("app_123", scanned_payload)

# Or verify without minting tokens (runtime check)
user = client.identity.verify_qr("app_123", scanned_payload)
print(user.first_name, user.role)
signup / create_user

Use identity.signup() (or create_user, which calls the same path) for Cognito/GCP accounts. Profile, QR, and PIN all live on the IdP — pass app_id on management and verify calls. Prefer email over legacy platform UUIDs.

Complete Examples

Basic Agent Invocation

from hikigai.appsdk import AppClient

client = AppClient()

# Get agent
agent = client.agent("customer-support")

# Invoke
response = agent.invoke("How do I reset my password?")
print(response.content)
print(f"Latency: {response.metadata.latency_ms}ms")

Streaming Responses

agent = client.agent("content-writer")

# Stream in real-time
print("Agent response: ", end="")
for chunk in agent.stream("Write a paragraph about AI"):
    print(chunk, end="", flush=True)
print()  # New line

Session-Based Chat Application

import uuid
from hikigai.appsdk import AppClient

def chat_session(user_id: str):
    """Start a chat session for a user."""
    client = AppClient()
    agent = client.agent("chatbot")
    
    # Create session-bound agent
    session = agent.with_session(user_id)
    
    print("Chat started. Type 'exit' to quit.")
    
    while True:
        user_input = input("You: ")
        if user_input.lower() == "exit":
            break
        
        # Invoke with session context
        response = session.invoke(user_input)
        print(f"Bot: {response.content}")

# Start chat for a user
chat_session(f"user-{uuid.uuid4()}")

Listing and Filtering Agents

client = AppClient()

# List all agents
agents = client.list_agents()
for agent in agents:
    print(f"{agent.name} (v{agent.version}): {agent.status}")

# Filter by category
medical_agents = client.list_agents(category="Medical Coding")

# Search
search_results = client.list_agents(search="diagnosis")

Error Handling

from hikigai.appsdk import (
    AppClient,
    AgentNotFoundError,
    InvocationError,
    RateLimitError
)

client = AppClient()

try:
    agent = client.agent("my-agent")
    response = agent.invoke("Hello")
    print(response.content)
    
except AgentNotFoundError:
    print("Agent not found. Check the agent ID.")
    
except InvocationError as e:
    print(f"Invocation failed: {e}")
    
except RateLimitError:
    print("Rate limit exceeded. Please wait.")
    
except Exception as e:
    print(f"Unexpected error: {e}")

Error Handling

AgentNotFoundError

Raised when the requested agent doesn't exist

from hikigai.appsdk import AgentNotFoundError

try:
    agent = client.agent("nonexistent-agent")
except AgentNotFoundError as e:
    print(f"Agent not found: {e}")

InvocationError

Raised when agent invocation fails

from hikigai.appsdk import InvocationError

try:
    response = agent.invoke("Complex query...")
except InvocationError as e:
    print(f"Invocation failed: {e}")

AuthenticationError

Raised when the API key is invalid or missing

from hikigai.appsdk import AuthenticationError, ConfigurationError

try:
    client = AppClient(api_key="invalid-key", project_id="proj_123")
except (AuthenticationError, ConfigurationError) as e:
    print(f"Auth/config failed: {e}")

RateLimitError

Raised when API rate limit is exceeded

from hikigai.appsdk import RateLimitError
import time

try:
    response = agent.invoke("Query")
except RateLimitError as e:
    print(f"Rate limit hit. Retry after: {e.retry_after}s")
    time.sleep(e.retry_after)
    response = agent.invoke("Query")  # Retry