Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 3ca7975e80 style: fix prettier formatting in webhooks-view 2026-03-04 13:58:57 +01:00
Nicolò Boschi 5af56339e7 fix(webhooks): include operation_id in task_payload so delivery is marked completed
The task_payload JSON was missing the operation_id field, causing execute_task
to see operation_id=None and skip _mark_operation_completed — leaving every
delivery row stuck in 'pending' forever.

Added a test that inserts a real async_operations row and verifies the status
transitions to 'completed' after a successful execute_task call.
2026-03-04 13:49:37 +01:00
Nicolò Boschi 3f819e7e11 feat(ui): add delete confirmation dialog for webhooks 2026-03-04 13:46:13 +01:00
Nicolò Boschi d2114453b5 fix(ui): add retain.completed to available webhook event types 2026-03-04 13:39:39 +01:00
Nicolò Boschi 847c73485f fix(webhooks): transactional outbox, observations_deleted tracking, sidebar
- Queue webhook delivery rows atomically with the primary operation using the
  transactional outbox pattern — prevents lost events on process crash:
  - Retain (sync + async): outbox_callback passed into orchestrator.retain_batch
    and called inside the DB transaction, replacing the post-commit fire call
  - Consolidation: new _mark_operation_completed_and_fire_webhook combines the
    status UPDATE and webhook INSERT in one transaction
  - Added fire_event_with_conn() to WebhookManager for in-connection delivery

- Track observations_deleted count in consolidation stats and expose it in the
  consolidation.completed webhook payload (was always None)

- Add Webhooks page to docs sidebar

- Document at-least-once delivery guarantee with operation_id dedup guidance
2026-03-04 13:33:15 +01:00
Nicolò Boschi 5637f09739 fix: remove max_retries from benchmark WorkerPoller call 2026-03-04 12:47:47 +01:00
Nicolò Boschi ef606ba224 fix: update tests for task-owned retry model and guard _webhook_manager attribute
- test_worker.py: test_executor_exception_triggers_retry now raises RetryTaskAt
  (plain exceptions are immediate failures in the new system); rename
  test_executor_exception_marks_failed_after_max_retries to
  test_executor_exception_marks_failed_immediately to reflect new semantics
- test_batch_api.py: remove max_retries kwarg from WorkerPoller constructor
- memory_engine.py: use getattr for _webhook_manager in _fire_retain_webhook
  to avoid AttributeError when engine is created without __init__ (tests)
2026-03-04 12:20:51 +01:00
Nicolò Boschi 94cbb4b57d feat: webhook system with task-owned retry, retain.completed event, and UI
- New webhook system: register per-bank webhooks with HMAC signing, configurable
  HTTP method/timeout/headers/params (http_config JSONB), and PATCH support
- Webhook deliveries run as async_operations (webhook_delivery type) with
  task-owned retry via RetryTaskAt exception and exponential backoff
  (60s / 5m / 30m / 2h / 8h, max 6 attempts)
- New retain.completed event fires per-document for both sync and async retain
- Delivery debug info (status code, response body) stored in result_metadata
- Control plane UI: webhooks tab per bank with create/edit/delete and a
  deliveries table with cursor pagination and expandable response details
- 28 webhook tests covering HMAC signing, delivery retries, CRUD endpoints,
  PATCH update, and retain.completed queuing
- Docs page at developer/api/webhooks documenting event payloads and delivery
- OpenAPI spec and all client SDKs (Python, TypeScript, Rust, Go) regenerated
2026-03-04 11:50:34 +01:00
Nicolò Boschi db18883352 ci: run test-doc-examples independently without waiting for test-rust-cli
Build the CLI directly in the job instead of downloading the artifact,
so test-doc-examples can start at the beginning in parallel with all other jobs.
2026-03-03 18:12:01 +01:00
Nicolò Boschi 8f329a1e70 fix(cookbook): preserve tag keys during sync, strip local .md links
- Fix extract_tags_from_readme/notebook to return dict[str,str] preserving
  sdk/topic keys instead of bare values, preventing topics like
  "Customer Service" from being misclassified as SDK
- Add strip_local_md_links() to remove relative .md references that
  would cause broken link errors in Docusaurus build
2026-03-03 17:52:57 +01:00
Nicolò Boschi 459f7b7025 doc: update cookbook 2026-03-03 17:44:29 +01:00
76 changed files with 11642 additions and 230 deletions
+16 -8
View File
@@ -1297,7 +1297,6 @@ jobs:
test-doc-examples:
runs-on: ubuntu-latest
needs: test-rust-cli
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY: /tmp/gcp-credentials.json
@@ -1315,14 +1314,23 @@ jobs:
PROJECT_ID=$(jq -r '.project_id' /tmp/gcp-credentials.json)
echo "HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=$PROJECT_ID" >> $GITHUB_ENV
- name: Download CLI artifact
uses: actions/download-artifact@v4
with:
name: hindsight-cli
path: /usr/local/bin
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Make CLI executable
run: chmod +x /usr/local/bin/hindsight
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
hindsight-cli/target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build CLI
working-directory: hindsight-cli
run: |
cargo build --release
cp target/release/hindsight /usr/local/bin/hindsight
- name: Install uv
uses: astral-sh/setup-uv@v5
@@ -0,0 +1,62 @@
"""Add webhooks table and next_retry_at to async_operations.
Webhook deliveries are handled as async_operations tasks (operation_type='webhook_delivery')
rather than a dedicated webhook_deliveries table.
Revision ID: e4f5a6b7c8d9
Revises: d2e3f4a5b6c7
Create Date: 2026-03-04
"""
from collections.abc import Sequence
from alembic import context, op
revision: str = "e4f5a6b7c8d9"
down_revision: str | Sequence[str] | None = "d2e3f4a5b6c7"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _get_schema_prefix() -> str:
schema = context.config.get_main_option("target_schema")
return f'"{schema}".' if schema else ""
def upgrade() -> None:
schema = _get_schema_prefix()
op.execute(
f"""
CREATE TABLE IF NOT EXISTS {schema}webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bank_id TEXT,
url TEXT NOT NULL,
secret TEXT,
event_types TEXT[] NOT NULL DEFAULT '{{}}',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""
)
# Index for bank-scoped webhook lookup
op.execute(f"CREATE INDEX IF NOT EXISTS idx_webhooks_bank_id ON {schema}webhooks(bank_id)")
# Add next_retry_at to async_operations for task-owned retry scheduling
op.execute(f"ALTER TABLE {schema}async_operations ADD COLUMN IF NOT EXISTS next_retry_at TIMESTAMPTZ NULL")
# Index for polling: status + next_retry_at
op.execute(
f"CREATE INDEX IF NOT EXISTS idx_async_operations_status_retry "
f"ON {schema}async_operations(status, next_retry_at)"
)
def downgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"DROP INDEX IF EXISTS {schema}idx_async_operations_status_retry")
op.execute(f"ALTER TABLE {schema}async_operations DROP COLUMN IF EXISTS next_retry_at")
op.execute(f"DROP INDEX IF EXISTS {schema}idx_webhooks_bank_id")
op.execute(f"DROP TABLE IF EXISTS {schema}webhooks")
@@ -0,0 +1,33 @@
"""Add http_config JSONB column to webhooks table.
Stores HTTP delivery configuration (method, timeout, headers, params) as a
single JSONB column rather than separate columns.
Revision ID: f7g8h9i0j1k2
Revises: e4f5a6b7c8d9
Create Date: 2026-03-04
"""
from collections.abc import Sequence
from alembic import context, op
revision: str = "f7g8h9i0j1k2"
down_revision: str | Sequence[str] | None = "e4f5a6b7c8d9"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _get_schema_prefix() -> str:
schema = context.config.get_main_option("target_schema")
return f'"{schema}".' if schema else ""
def upgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"ALTER TABLE {schema}webhooks ADD COLUMN IF NOT EXISTS http_config JSONB NOT NULL DEFAULT '{{}}'")
def downgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"ALTER TABLE {schema}webhooks DROP COLUMN IF EXISTS http_config")
+435 -1
View File
@@ -1627,6 +1627,123 @@ class VersionResponse(BaseModel):
features: FeaturesInfo = Field(description="Enabled feature flags")
# =========================================================================
# Webhook Models
# =========================================================================
from hindsight_api.webhooks.models import WebhookHttpConfig
class CreateWebhookRequest(BaseModel):
"""Request model for registering a webhook."""
url: str = Field(description="HTTP(S) endpoint URL to deliver events to")
secret: str | None = Field(default=None, description="HMAC-SHA256 signing secret (optional)")
event_types: list[str] = Field(
default=["consolidation.completed"],
description="List of event types to deliver. Currently supported: 'consolidation.completed'",
)
enabled: bool = Field(default=True, description="Whether this webhook is active")
http_config: WebhookHttpConfig = Field(
default_factory=WebhookHttpConfig,
description="HTTP delivery configuration (method, timeout, headers, params)",
)
class WebhookResponse(BaseModel):
"""Response model for a webhook."""
id: str
bank_id: str | None
url: str
secret: str | None = Field(default=None, description="Signing secret (redacted in responses)")
event_types: list[str]
enabled: bool
http_config: WebhookHttpConfig = Field(default_factory=WebhookHttpConfig)
created_at: str | None = None
updated_at: str | None = None
class UpdateWebhookRequest(BaseModel):
"""Request model for updating a webhook. Only provided fields are updated."""
url: str | None = Field(default=None, description="HTTP(S) endpoint URL")
secret: str | None = Field(
default=None, description="HMAC-SHA256 signing secret. Omit to keep existing; send null to clear."
)
event_types: list[str] | None = Field(default=None, description="List of event types")
enabled: bool | None = Field(default=None, description="Whether this webhook is active")
http_config: WebhookHttpConfig | None = Field(default=None, description="HTTP delivery configuration")
class WebhookListResponse(BaseModel):
"""Response model for listing webhooks."""
items: list[WebhookResponse]
class WebhookDeliveryResponse(BaseModel):
"""Response model for a webhook delivery record."""
id: str
webhook_id: str | None
url: str
event_type: str
status: str
attempts: int
next_retry_at: str | None = None
last_error: str | None = None
last_response_status: int | None = None
last_response_body: str | None = None
last_attempt_at: str | None = None
created_at: str | None = None
updated_at: str | None = None
@classmethod
def from_async_operation_row(cls, row: dict) -> "WebhookDeliveryResponse":
import json as _json
raw = row["task_payload"]
if isinstance(raw, str):
task_payload = _json.loads(raw)
elif isinstance(raw, dict):
task_payload = raw
else:
task_payload = {}
raw_meta = row.get("result_metadata")
if isinstance(raw_meta, str):
result_metadata = _json.loads(raw_meta) if raw_meta else {}
elif isinstance(raw_meta, dict):
result_metadata = raw_meta
else:
result_metadata = {}
return cls(
id=str(row["operation_id"]),
webhook_id=task_payload.get("webhook_id"),
url=task_payload.get("url", ""),
event_type=task_payload.get("event_type", ""),
status=row["status"],
attempts=row["retry_count"] + 1,
next_retry_at=row["next_retry_at"],
last_error=row["error_message"],
last_response_status=result_metadata.get("last_status_code"),
last_response_body=result_metadata.get("last_response_body"),
last_attempt_at=result_metadata.get("last_attempt_at"),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
class WebhookDeliveryListResponse(BaseModel):
"""Response model for listing webhook deliveries."""
items: list[WebhookDeliveryResponse]
next_cursor: str | None = None
def create_app(
memory: MemoryEngine,
initialize_memory: bool = True,
@@ -1726,7 +1843,6 @@ def create_app(
worker_id=worker_id,
executor=memory.execute_task,
poll_interval_ms=config.worker_poll_interval_ms,
max_retries=config.worker_max_retries,
schema=schema,
tenant_extension=memory._tenant_extension,
max_slots=config.worker_max_slots,
@@ -3756,6 +3872,318 @@ def _register_routes(app: FastAPI):
logger.error(f"Error in POST /v1/default/banks/{bank_id}/consolidate: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
# =========================================================================
# Webhook Endpoints
# =========================================================================
@app.post(
"/v1/default/banks/{bank_id}/webhooks",
response_model=WebhookResponse,
summary="Register webhook",
description="Register a webhook endpoint to receive event notifications for this bank.",
operation_id="create_webhook",
tags=["Webhooks"],
status_code=201,
)
async def api_create_webhook(
bank_id: str,
request: CreateWebhookRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Register a webhook for a bank."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
webhook_id = uuid.uuid4()
now = datetime.utcnow().isoformat() + "Z"
row = await pool.fetchrow(
f"""
INSERT INTO {fq_table("webhooks")}
(id, bank_id, url, secret, event_types, enabled, http_config, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, NOW(), NOW())
RETURNING id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
""",
webhook_id,
bank_id,
request.url,
request.secret,
request.event_types,
request.enabled,
request.http_config.model_dump_json(),
)
return WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None, # Never return secret in responses
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in POST /v1/default/banks/{bank_id}/webhooks: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/webhooks",
response_model=WebhookListResponse,
summary="List webhooks",
description="List all webhooks registered for a bank.",
operation_id="list_webhooks",
tags=["Webhooks"],
)
async def api_list_webhooks(
bank_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""List webhooks for a bank."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
rows = await pool.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
FROM {fq_table("webhooks")}
WHERE bank_id = $1
ORDER BY created_at
""",
bank_id,
)
return WebhookListResponse(
items=[
WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None, # Never return secret in responses
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
for row in rows
]
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/webhooks: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
response_model=DeleteResponse,
summary="Delete webhook",
description="Remove a registered webhook.",
operation_id="delete_webhook",
tags=["Webhooks"],
)
async def api_delete_webhook(
bank_id: str,
webhook_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""Delete a webhook."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
result = await pool.execute(
f"DELETE FROM {fq_table('webhooks')} WHERE id = $1 AND bank_id = $2",
uuid.UUID(webhook_id),
bank_id,
)
deleted = int(result.split()[-1]) if result else 0
if deleted == 0:
raise HTTPException(status_code=404, detail="Webhook not found")
return DeleteResponse(success=True)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/webhooks/{webhook_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.patch(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
response_model=WebhookResponse,
summary="Update webhook",
description="Update one or more fields of a registered webhook. Only provided fields are changed.",
operation_id="update_webhook",
tags=["Webhooks"],
)
async def api_update_webhook(
bank_id: str,
webhook_id: str,
request: UpdateWebhookRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Update a webhook's fields (PATCH semantics — only sent fields are updated)."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
set_clauses: list[str] = []
params: list = [uuid.UUID(webhook_id), bank_id]
fields = request.model_fields_set
if "url" in fields:
params.append(request.url)
set_clauses.append(f"url = ${len(params)}")
if "secret" in fields:
params.append(request.secret)
set_clauses.append(f"secret = ${len(params)}")
if "event_types" in fields:
params.append(request.event_types)
set_clauses.append(f"event_types = ${len(params)}")
if "enabled" in fields:
params.append(request.enabled)
set_clauses.append(f"enabled = ${len(params)}")
if "http_config" in fields:
params.append(request.http_config.model_dump_json())
set_clauses.append(f"http_config = ${len(params)}::jsonb")
if not set_clauses:
raise HTTPException(status_code=422, detail="No fields provided to update")
set_clauses.append("updated_at = NOW()")
row = await pool.fetchrow(
f"""
UPDATE {fq_table("webhooks")}
SET {", ".join(set_clauses)}
WHERE id = $1 AND bank_id = $2
RETURNING id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
""",
*params,
)
if not row:
raise HTTPException(status_code=404, detail="Webhook not found")
return WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None,
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/webhooks/{webhook_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries",
response_model=WebhookDeliveryListResponse,
summary="List webhook deliveries",
description="Inspect delivery history for a webhook (useful for debugging).",
operation_id="list_webhook_deliveries",
tags=["Webhooks"],
)
async def api_list_webhook_deliveries(
bank_id: str,
webhook_id: str,
limit: int = Query(default=50, le=200, description="Maximum number of deliveries to return"),
cursor: str | None = Query(default=None, description="Pagination cursor (created_at of last item)"),
request_context: RequestContext = Depends(get_request_context),
):
"""List deliveries for a specific webhook, newest first. Use next_cursor for pagination."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
# Verify webhook belongs to this bank
webhook_row = await pool.fetchrow(
f"SELECT id FROM {fq_table('webhooks')} WHERE id = $1 AND bank_id = $2",
uuid.UUID(webhook_id),
bank_id,
)
if not webhook_row:
raise HTTPException(status_code=404, detail="Webhook not found")
# Fetch limit+1 to detect if there's a next page
fetch_limit = limit + 1
if cursor:
rows = await pool.fetch(
f"""
SELECT operation_id, status, retry_count, next_retry_at::text,
error_message, task_payload, result_metadata::text, created_at::text, updated_at::text
FROM {fq_table("async_operations")}
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
AND created_at < $3::timestamptz
ORDER BY created_at DESC
LIMIT $4
""",
bank_id,
webhook_id,
cursor,
fetch_limit,
)
else:
rows = await pool.fetch(
f"""
SELECT operation_id, status, retry_count, next_retry_at::text,
error_message, task_payload, result_metadata::text, created_at::text, updated_at::text
FROM {fq_table("async_operations")}
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
ORDER BY created_at DESC
LIMIT $3
""",
bank_id,
webhook_id,
fetch_limit,
)
has_more = len(rows) > limit
page = rows[:limit]
next_cursor = page[-1]["created_at"] if has_more and page else None
return WebhookDeliveryListResponse(
items=[WebhookDeliveryResponse.from_async_operation_row(dict(row)) for row in page],
next_cursor=next_cursor,
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/v1/default/banks/{bank_id}/memories",
response_model=RetainResponse,
@@ -3848,6 +4276,12 @@ def _register_routes(app: FastAPI):
document_tags=request.document_tags,
request_context=request_context,
return_usage=True,
outbox_callback=app.state.memory._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id=None,
schema=request_context.tenant_id,
),
)
return RetainResponse.model_validate(
+32
View File
@@ -294,6 +294,12 @@ ENV_CONSOLIDATION_LLM_BATCH_SIZE = "HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE"
ENV_CONSOLIDATION_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS"
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
# Webhook configuration (global, static - server-level only)
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
ENV_WEBHOOK_SECRET = "HINDSIGHT_API_WEBHOOK_SECRET"
ENV_WEBHOOK_EVENT_TYPES = "HINDSIGHT_API_WEBHOOK_EVENT_TYPES"
ENV_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS = "HINDSIGHT_API_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS"
# Optimization flags
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
@@ -497,6 +503,12 @@ Use this tool PROACTIVELY to:
# Default embedding dimension (used by initial migration, adjusted at runtime)
EMBEDDING_DIMENSION = DEFAULT_EMBEDDING_DIMENSION
# Webhook configuration defaults
DEFAULT_WEBHOOK_URL = None # None = no global webhook configured
DEFAULT_WEBHOOK_SECRET = None # None = no signing
DEFAULT_WEBHOOK_EVENT_TYPES = "consolidation.completed" # Comma-separated; default = all supported events
DEFAULT_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS = 30 # How often to poll for pending deliveries
class JsonFormatter(logging.Formatter):
"""JSON formatter for structured logging.
@@ -750,6 +762,12 @@ class HindsightConfig:
otel_service_name: str
otel_deployment_environment: str
# Webhook configuration (static - server-level only, not per-bank)
webhook_url: str | None # Global webhook URL (None = disabled)
webhook_secret: str | None # HMAC signing secret (None = unsigned)
webhook_event_types: list[str] # Event types to deliver globally
webhook_delivery_poll_interval_seconds: int # How often the delivery worker polls
# Class-level sets for configuration categorization
# CREDENTIAL_FIELDS: Never exposed via API, never configurable per-tenant/bank
@@ -1187,6 +1205,20 @@ class HindsightConfig:
otel_exporter_otlp_headers=os.getenv(ENV_OTEL_EXPORTER_OTLP_HEADERS) or None,
otel_service_name=os.getenv(ENV_OTEL_SERVICE_NAME, DEFAULT_OTEL_SERVICE_NAME),
otel_deployment_environment=os.getenv(ENV_OTEL_DEPLOYMENT_ENVIRONMENT, DEFAULT_OTEL_DEPLOYMENT_ENVIRONMENT),
# Webhook configuration (static, server-level only)
webhook_url=os.getenv(ENV_WEBHOOK_URL) or DEFAULT_WEBHOOK_URL,
webhook_secret=os.getenv(ENV_WEBHOOK_SECRET) or DEFAULT_WEBHOOK_SECRET,
webhook_event_types=[
t.strip()
for t in os.getenv(ENV_WEBHOOK_EVENT_TYPES, DEFAULT_WEBHOOK_EVENT_TYPES).split(",")
if t.strip()
],
webhook_delivery_poll_interval_seconds=int(
os.getenv(
ENV_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS,
str(DEFAULT_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS),
)
),
)
config.validate()
return config
@@ -180,11 +180,12 @@ async def run_consolidation_job(
perf.log(f"[1] Found {total_count} pending memories to consolidate")
# Process each memory with individual commits for crash recovery
stats = {
stats: dict[str, int] = {
"memories_processed": 0,
"observations_created": 0,
"observations_updated": 0,
"observations_merged": 0,
"observations_deleted": 0,
"actions_executed": 0,
"skipped": 0,
}
@@ -273,11 +274,12 @@ async def run_consolidation_job(
# explicit list[list[str]]
obs_tags_list = _obs_parsed
batch_deleted: int = 0
if obs_tags_list:
# Multi-pass: run one observation consolidation pass per tag set
results = []
for obs_tags in obs_tags_list:
pass_results = await _process_memory_batch(
pass_results, pass_deleted = await _process_memory_batch(
conn=conn,
memory_engine=memory_engine,
llm_config=llm_config,
@@ -288,6 +290,7 @@ async def run_consolidation_job(
config=config,
obs_tags_override=obs_tags,
)
batch_deleted += pass_deleted
# Merge results: prefer non-skipped actions
if not results:
results = pass_results
@@ -315,7 +318,7 @@ async def run_consolidation_job(
}
else:
# Normal single pass using the memory's own tags
results = await _process_memory_batch(
results, batch_deleted = await _process_memory_batch(
conn=conn,
memory_engine=memory_engine,
llm_config=llm_config,
@@ -325,6 +328,7 @@ async def run_consolidation_job(
perf=perf,
config=config,
)
stats["observations_deleted"] += batch_deleted
await conn.executemany(
f"UPDATE {fq_table('memory_units')} SET consolidated_at = NOW() WHERE id = $1",
@@ -521,7 +525,7 @@ async def _process_memory_batch(
perf: ConsolidationPerfLog | None = None,
config: Any = None,
obs_tags_override: list[str] | None = None,
) -> list[dict[str, Any]]:
) -> tuple[list[dict[str, Any]], int]:
"""
Process a batch of memories in a single LLM call.
@@ -656,6 +660,7 @@ async def _process_memory_batch(
for m in source_mems:
per_memory_updated.add(str(m["id"]))
deleted_count = 0
for delete in llm_result.deletes:
# Security: the observation must be present in the unioned recall
if not any(str(obs.id) == delete.observation_id for obs in union_observations):
@@ -664,6 +669,7 @@ async def _process_memory_batch(
)
continue
await _execute_delete_action(conn=conn, bank_id=bank_id, observation_id=delete.observation_id)
deleted_count += 1
# Build per-memory result dicts for the stats tracker in the outer loop
results: list[dict[str, Any]] = []
@@ -680,7 +686,7 @@ async def _process_memory_batch(
else:
results.append({"action": "skipped", "reason": "no_durable_knowledge"})
return results
return results, deleted_count
def _min_date(dates: "Any") -> "datetime | None":
@@ -15,15 +15,19 @@ import json
import logging
import time
import uuid
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any
import asyncpg
import httpx
import tiktoken
from ..config import get_config
from ..metrics import get_metrics_collector
from ..tracing import create_operation_span
from ..utils import mask_network_location
from ..worker.exceptions import RetryTaskAt
from .db_budget import budgeted_operation
from .operation_metadata import (
BatchRetainChildMetadata,
@@ -359,6 +363,10 @@ class MemoryEngine(MemoryEngineInterface):
self._run_migrations = run_migrations
self._retain_entity_lookup = config.retain_entity_lookup
# Webhook manager (will be created in initialize() after pool is ready)
self._webhook_manager = None
self._http_client: httpx.AsyncClient | None = None
# Initialize entity resolver (will be created in initialize())
self.entity_resolver = None
@@ -582,6 +590,12 @@ class MemoryEngine(MemoryEngineInterface):
document_tags=document_tags,
request_context=context,
operation_id=operation_id,
outbox_callback=self._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id=operation_id,
schema=context.tenant_id,
),
)
# If this retain was triggered by file conversion, update document with file metadata
@@ -778,6 +792,7 @@ class MemoryEngine(MemoryEngineInterface):
)
logger.info(f"[CONSOLIDATION] bank={bank_id} completed: {result.get('memories_processed', 0)} processed")
return result
async def _handle_refresh_mental_model(self, task_dict: dict[str, Any]):
"""
@@ -949,15 +964,18 @@ class MemoryEngine(MemoryEngineInterface):
logger.error(f"Failed to check operation status {operation_id}: {e}")
# Continue with processing if we can't check status
consolidation_result: dict | None = None
try:
if task_type == "batch_retain":
await self._handle_batch_retain(task_dict)
elif task_type == "file_convert_retain":
await self._handle_file_convert_retain(task_dict)
elif task_type == "consolidation":
await self._handle_consolidation(task_dict)
consolidation_result = await self._handle_consolidation(task_dict)
elif task_type == "refresh_mental_model":
await self._handle_refresh_mental_model(task_dict)
elif task_type == "webhook_delivery":
await self._handle_webhook_delivery(task_dict)
else:
logger.error(f"Unknown task type: {task_type}")
# Don't retry unknown task types
@@ -967,9 +985,22 @@ class MemoryEngine(MemoryEngineInterface):
# Task succeeded - mark operation as completed
# file_convert_retain marks itself as completed in a transaction, skip double-marking
if operation_id and task_type != "file_convert_retain":
await self._mark_operation_completed(operation_id)
if operation_id and task_type not in ("file_convert_retain",):
if task_type == "consolidation":
# Atomically mark completed AND queue webhook delivery in one transaction
await self._mark_operation_completed_and_fire_webhook(
operation_id=operation_id,
bank_id=task_dict.get("bank_id", ""),
status="completed",
result=consolidation_result,
schema=schema,
)
else:
await self._mark_operation_completed(operation_id)
except RetryTaskAt:
# Task-owned retry: let the poller handle scheduling
raise
except Exception as e:
logger.error(f"Task execution failed: {task_type}, error: {e}")
import traceback
@@ -984,10 +1015,193 @@ class MemoryEngine(MemoryEngineInterface):
if operation_id:
await self._mark_operation_failed(operation_id, str(e), error_traceback)
else:
# Retryable: re-raise so the worker poller handles retry/fail via _retry_or_fail,
# which correctly resets status='pending' and increments the DB retry_count.
if task_type == "consolidation" and operation_id:
# Fire failure webhook (non-transactional — operation not yet marked failed;
# poller will mark it failed after this raise)
await self._fire_consolidation_webhook(
bank_id=task_dict.get("bank_id", ""),
operation_id=operation_id,
status="failed",
result=None,
error_message=str(e),
schema=schema,
)
# Retryable: use RetryTaskAt if under the retry limit, else re-raise (poller marks failed)
retry_count = task_dict.get("_retry_count", 0)
if retry_count < 3:
raise RetryTaskAt(retry_at=datetime.now(UTC) + timedelta(seconds=60), message=str(e))
raise
async def _fire_consolidation_webhook(
self,
bank_id: str,
operation_id: str,
status: str,
result: dict | None,
error_message: str | None = None,
schema: str | None = None,
) -> None:
"""Fire a consolidation webhook event. Non-fatal - logs errors but does not raise."""
if not self._webhook_manager:
return
try:
from datetime import datetime, timezone
from ..webhooks.models import ConsolidationEventData, WebhookEvent, WebhookEventType
data = ConsolidationEventData(
observations_created=result.get("observations_created") if result else None,
observations_updated=result.get("observations_updated") if result else None,
observations_deleted=result.get("observations_deleted") if result else None,
error_message=error_message,
)
event = WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=operation_id,
status=status,
timestamp=datetime.now(timezone.utc),
data=data,
)
await self._webhook_manager.fire_event(event, schema=schema)
except Exception as e:
logger.error(f"Failed to fire consolidation webhook for operation {operation_id}: {e}")
def _build_retain_outbox_callback(
self,
bank_id: str,
contents: list[dict],
operation_id: str | None,
schema: str | None = None,
) -> "Callable[[asyncpg.Connection], Awaitable[None]] | None":
"""Build a transactional outbox callback for retain.completed webhook events.
Returns a coroutine function that queues one webhook delivery row per content
item using the provided connection (inside the retain transaction). Returns None
if no webhook manager is configured.
"""
webhook_manager = getattr(self, "_webhook_manager", None)
if not webhook_manager:
return None
from ..webhooks.models import RetainEventData, WebhookEvent, WebhookEventType
now = datetime.now(UTC)
op_id = operation_id or uuid.uuid4().hex
events = []
for content in contents:
doc_id = content.get("document_id")
tags = content.get("tags")
data = RetainEventData(
document_id=doc_id,
tags=tags if isinstance(tags, list) else None,
)
events.append(
WebhookEvent(
event=WebhookEventType.RETAIN_COMPLETED,
bank_id=bank_id,
operation_id=op_id,
status="completed",
timestamp=now,
data=data,
)
)
async def _callback(conn: asyncpg.Connection) -> None:
for event in events:
await webhook_manager.fire_event_with_conn(event, conn, schema=schema)
return _callback
async def _update_webhook_delivery_metadata(
self, operation_id: str, status_code: int | None, response_body: str | None
) -> None:
"""Persist last HTTP attempt info into async_operations.result_metadata."""
try:
pool = await self._get_pool()
meta = json.dumps(
{
"last_status_code": status_code,
"last_response_body": (response_body or "")[:2048],
"last_attempt_at": datetime.now(UTC).isoformat(),
}
)
async with acquire_with_retry(pool) as conn:
await conn.execute(
f"UPDATE {fq_table('async_operations')} SET result_metadata = $2::jsonb, updated_at = now() WHERE operation_id = $1",
uuid.UUID(operation_id),
meta,
)
except Exception as meta_err:
logger.debug(f"Failed to update webhook delivery metadata: {meta_err}")
async def _handle_webhook_delivery(self, task_dict: dict[str, Any]) -> None:
"""Deliver a webhook event via HTTP.
Raises RetryTaskAt to schedule a retry on failure (up to MAX_ATTEMPTS).
Raises the original exception when retries are exhausted (poller marks failed).
Response status code and body are stored in result_metadata for debugging.
"""
from ..webhooks.manager import MAX_ATTEMPTS, RETRY_DELAYS
from ..webhooks.models import WebhookHttpConfig
url = task_dict["url"]
secret = task_dict.get("secret")
event_type = task_dict["event_type"]
raw_payload = task_dict["payload"]
retry_count = task_dict.get("_retry_count", 0)
operation_id: str | None = task_dict.get("_operation_id")
http_config = WebhookHttpConfig.model_validate(task_dict.get("http_config") or {})
if isinstance(raw_payload, dict):
payload_bytes = json.dumps(raw_payload).encode()
else:
payload_bytes = str(raw_payload).encode()
headers: dict[str, str] = {
"Content-Type": "application/json",
"X-Hindsight-Event": event_type,
**http_config.headers,
}
if secret and self._webhook_manager:
headers["X-Hindsight-Signature"] = self._webhook_manager._sign_payload(secret, payload_bytes)
if self._http_client is None:
raise RuntimeError("HTTP client not initialized")
response = None
try:
request_kwargs: dict[str, Any] = {
"headers": headers,
"params": http_config.params if http_config.params else None,
"timeout": http_config.timeout_seconds,
}
if http_config.method.upper() == "GET":
response = await self._http_client.get(url, **request_kwargs)
else:
response = await self._http_client.post(url, content=payload_bytes, **request_kwargs)
response.raise_for_status()
if operation_id:
await self._update_webhook_delivery_metadata(operation_id, response.status_code, response.text)
except Exception as e:
status_code = response.status_code if response is not None else None
response_body = response.text if response is not None else None
if operation_id:
await self._update_webhook_delivery_metadata(operation_id, status_code, response_body)
if retry_count >= MAX_ATTEMPTS - 1:
logger.error(
f"webhook_delivery permanently_failed url={url} attempts={retry_count + 1} "
f"status_code={status_code} error={e}"
)
raise
delay = RETRY_DELAYS[retry_count] if retry_count < len(RETRY_DELAYS) else RETRY_DELAYS[-1]
retry_at = datetime.now(UTC) + timedelta(seconds=delay)
logger.warning(
f"webhook_delivery failed url={url} attempt={retry_count + 1}/{MAX_ATTEMPTS} "
f"status_code={status_code} retry_in={delay}s error={e}"
)
raise RetryTaskAt(retry_at=retry_at, message=str(e))
async def _delete_operation_record(self, operation_id: str):
"""Helper to delete an operation record from the database."""
try:
@@ -1058,6 +1272,58 @@ class MemoryEngine(MemoryEngineInterface):
except Exception as e:
logger.error(f"Failed to mark operation as completed {operation_id}: {e}")
async def _mark_operation_completed_and_fire_webhook(
self,
operation_id: str,
bank_id: str,
status: str,
result: dict | None,
schema: str | None = None,
error_message: str | None = None,
) -> None:
"""Mark an operation as completed and queue webhook deliveries in a single transaction.
Uses the transactional outbox pattern: the webhook delivery row is inserted in the
same database transaction as the status update. This guarantees at-least-once delivery
even if the process crashes immediately after committing.
"""
from ..webhooks.models import ConsolidationEventData, WebhookEvent, WebhookEventType
try:
pool = await self._get_pool()
async with acquire_with_retry(pool) as conn:
async with conn.transaction():
await conn.execute(
f"""
UPDATE {fq_table("async_operations")}
SET status = 'completed', updated_at = NOW(), completed_at = NOW()
WHERE operation_id = $1
""",
uuid.UUID(operation_id),
)
logger.info(f"Marked async operation as completed: {operation_id}")
await self._maybe_update_parent_operation(operation_id, conn)
# Queue webhook deliveries inside the same transaction
if self._webhook_manager:
data = ConsolidationEventData(
observations_created=result.get("observations_created") if result else None,
observations_updated=result.get("observations_updated") if result else None,
observations_deleted=result.get("observations_deleted") if result else None,
error_message=error_message,
)
event = WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=operation_id,
status=status,
timestamp=datetime.now(UTC),
data=data,
)
await self._webhook_manager.fire_event_with_conn(event, conn, schema=schema)
except Exception as e:
logger.error(f"Failed to mark operation completed and fire webhook {operation_id}: {e}")
async def _maybe_update_parent_operation(self, child_operation_id: str, conn):
"""Check if this is a child operation and update parent status if all siblings are done.
@@ -1381,6 +1647,32 @@ class MemoryEngine(MemoryEngineInterface):
else:
logger.debug("Iris parser not registered (VECTORIZE_TOKEN or VECTORIZE_ORG_ID not set)")
# Initialize webhook manager
from ..webhooks import WebhookManager
from ..webhooks.models import WebhookConfig
webhook_global: list[WebhookConfig] = []
if config.webhook_url:
webhook_global = [
WebhookConfig(
id="", # No DB row for env-configured global webhook
bank_id=None,
url=config.webhook_url,
secret=config.webhook_secret,
event_types=config.webhook_event_types,
enabled=True,
)
]
self._webhook_manager = WebhookManager(
pool=self._pool,
global_webhooks=webhook_global,
tenant_extension=self._tenant_extension,
)
logger.debug("Webhook manager initialized")
# Long-lived HTTP client for webhook delivery tasks
self._http_client = httpx.AsyncClient(timeout=30.0)
# Set executor for task backend and initialize
self._task_backend.set_executor(self.execute_task)
await self._task_backend.initialize()
@@ -1440,6 +1732,11 @@ class MemoryEngine(MemoryEngineInterface):
# Shutdown task backend
await self._task_backend.shutdown()
# Close HTTP client used for webhook delivery
if self._http_client is not None:
await self._http_client.aclose()
self._http_client = None
# Close pool
if self._pool is not None:
self._pool.terminate()
@@ -1580,6 +1877,7 @@ class MemoryEngine(MemoryEngineInterface):
document_tags: list[str] | None = None,
return_usage: bool = False,
operation_id: str | None = None,
outbox_callback: "Callable[[asyncpg.Connection], Awaitable[None]] | None" = None,
):
"""
Store multiple content items as memory units in ONE batch operation.
@@ -1728,6 +2026,9 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score=confidence_score,
document_tags=document_tags,
operation_id=operation_id,
# Outbox callback runs inside the last sub-batch's transaction so the
# webhook delivery row is committed atomically with the final retain data.
outbox_callback=outbox_callback if i == len(sub_batches) else None,
)
all_results.extend(sub_results)
total_usage = total_usage + sub_usage
@@ -1749,6 +2050,7 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score=confidence_score,
document_tags=document_tags,
operation_id=operation_id,
outbox_callback=outbox_callback,
)
# Call post-operation hook if validator is configured
@@ -1799,6 +2101,7 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score: float | None = None,
document_tags: list[str] | None = None,
operation_id: str | None = None,
outbox_callback: "Callable[[asyncpg.Connection], Awaitable[None]] | None" = None,
) -> tuple[list[list[str]], "TokenUsage"]:
"""
Internal method for batch processing without chunking logic.
@@ -1849,6 +2152,7 @@ class MemoryEngine(MemoryEngineInterface):
config=resolved_config,
operation_id=operation_id,
schema=request_context.tenant_id if request_context else None,
outbox_callback=outbox_callback,
)
def recall(
@@ -7,6 +7,7 @@ Coordinates all retain pipeline modules to store memories efficiently.
import logging
import time
import uuid
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from typing import Any
@@ -52,6 +53,8 @@ def parse_datetime_flexible(value: Any) -> datetime:
raise TypeError(f"Expected datetime or string, got {type(value).__name__}")
import asyncpg
from ..response_models import TokenUsage
from . import (
chunk_storage,
@@ -82,6 +85,7 @@ async def retain_batch(
document_tags: list[str] | None = None,
operation_id: str | None = None,
schema: str | None = None,
outbox_callback: Callable[["asyncpg.Connection"], Awaitable[None]] | None = None,
) -> tuple[list[list[str]], TokenUsage]:
"""
Process a batch of content through the retain pipeline.
@@ -484,6 +488,11 @@ async def retain_batch(
# Map results back to original content items
result_unit_ids = _map_results_to_contents(contents, extracted_facts, unit_ids)
# Transactional outbox: queue any side-effect tasks (e.g. webhook deliveries)
# inside the same transaction so they are atomically committed with the retain data.
if outbox_callback:
await outbox_callback(conn)
# Flush entity stats (mention_count / last_seen) now that the transaction
# has committed. Uses a fresh pool connection — no locks held.
await entity_resolver.flush_pending_stats()
+4
View File
@@ -307,6 +307,10 @@ def main():
otel_exporter_otlp_headers=config.otel_exporter_otlp_headers,
otel_service_name=config.otel_service_name,
otel_deployment_environment=config.otel_deployment_environment,
webhook_url=config.webhook_url,
webhook_secret=config.webhook_secret,
webhook_event_types=config.webhook_event_types,
webhook_delivery_poll_interval_seconds=config.webhook_delivery_poll_interval_seconds,
)
config.configure_logging()
if not args.daemon:
@@ -0,0 +1,13 @@
"""Webhook system for Hindsight API event notifications."""
from .manager import WebhookManager
from .models import ConsolidationEventData, RetainEventData, WebhookConfig, WebhookEvent, WebhookEventType
__all__ = [
"WebhookManager",
"WebhookConfig",
"WebhookEvent",
"WebhookEventType",
"ConsolidationEventData",
"RetainEventData",
]
@@ -0,0 +1,238 @@
"""Webhook manager for delivering event notifications."""
import hashlib
import hmac
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING
import asyncpg
from .models import WebhookConfig, WebhookEvent, WebhookHttpConfig
if TYPE_CHECKING:
from hindsight_api.extensions.tenant import TenantExtension
logger = logging.getLogger(__name__)
# Retry delay schedule in seconds: 5 retries after the first attempt.
# Fast early retries catch transient failures; later retries handle longer outages.
RETRY_DELAYS = [5, 300, 1800, 7200, 18000]
MAX_ATTEMPTS = len(RETRY_DELAYS) + 1 # first attempt + len(RETRY_DELAYS) retries
def _fq_table(table: str, schema: str | None = None) -> str:
"""Get fully-qualified table name with optional schema prefix."""
if schema:
return f'"{schema}".{table}'
return table
def _parse_http_config(value: str | dict | None) -> WebhookHttpConfig:
"""Parse http_config column value (JSONB returned as text or dict) into a model."""
if value is None:
return WebhookHttpConfig()
if isinstance(value, str):
return WebhookHttpConfig.model_validate_json(value)
return WebhookHttpConfig.model_validate(value)
class WebhookManager:
"""
Manages webhook registration and event firing.
Supports both global webhooks (configured via env vars) and per-bank
webhooks stored in the database. Deliveries are queued as async_operations
tasks (operation_type='webhook_delivery') and picked up by the worker poller.
"""
def __init__(
self,
pool: asyncpg.Pool,
global_webhooks: list[WebhookConfig],
tenant_extension: "TenantExtension | None" = None,
):
self._pool = pool
self._global_webhooks = global_webhooks
self._tenant_extension = tenant_extension
def _sign_payload(self, secret: str, payload_bytes: bytes) -> str:
"""Compute HMAC-SHA256 signature for a payload."""
return "sha256=" + hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
async def fire_event(self, event: WebhookEvent, schema: str | None = None) -> None:
"""
Queue webhook deliveries for an event as async_operations tasks.
Loads per-bank and global webhooks, inserts pending webhook_delivery tasks for
any webhook whose event_types list matches the fired event type. The worker
poller picks these up and calls MemoryEngine._handle_webhook_delivery().
Args:
event: The event to deliver.
schema: Database schema (for multi-tenant). None = default schema.
"""
webhook_table = _fq_table("webhooks", schema)
ops_table = _fq_table("async_operations", schema)
now = datetime.now(timezone.utc)
payload_str = event.model_dump_json()
try:
# Load per-bank webhooks from DB (bank-specific + global NULL rows)
rows = await self._pool.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled, http_config::text
FROM {webhook_table}
WHERE (bank_id = $1 OR bank_id IS NULL) AND enabled = true
""",
event.bank_id,
)
db_webhooks = [
WebhookConfig(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=row["secret"],
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=_parse_http_config(row["http_config"]),
)
for row in rows
]
# Merge with global webhooks from env config
all_webhooks = self._global_webhooks + db_webhooks
matched = 0
for webhook in all_webhooks:
if not webhook.enabled:
continue
if event.event.value not in webhook.event_types:
continue
operation_id = uuid.uuid4()
webhook_id = webhook.id if webhook.id else None
task_payload = json.dumps(
{
"type": "webhook_delivery",
"operation_id": str(operation_id),
"bank_id": event.bank_id,
"url": webhook.url,
"secret": webhook.secret,
"event_type": event.event.value,
"payload": payload_str,
"webhook_id": webhook_id,
"http_config": webhook.http_config.model_dump(),
}
)
await self._pool.execute(
f"""
INSERT INTO {ops_table}
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'pending', $3::jsonb, '{{}}'::jsonb, $4, $4)
""",
operation_id,
event.bank_id,
task_payload,
now,
)
matched += 1
logger.debug(f"Fired webhook event {event.event} for bank {event.bank_id}: {matched} delivery(ies) queued")
except Exception as e:
logger.error(f"Failed to queue webhook deliveries for event {event.event}: {e}")
async def fire_event_with_conn(
self, event: WebhookEvent, conn: asyncpg.Connection, schema: str | None = None
) -> None:
"""
Queue webhook deliveries within an existing database connection/transaction.
Identical to fire_event() but uses the provided connection instead of acquiring
one from the pool. Use this to atomically insert delivery tasks in the same
transaction as the primary operation (transactional outbox pattern).
Args:
event: The event to deliver.
conn: Existing asyncpg connection (may be inside an active transaction).
schema: Database schema (for multi-tenant). None = default schema.
"""
webhook_table = _fq_table("webhooks", schema)
ops_table = _fq_table("async_operations", schema)
now = datetime.now(timezone.utc)
payload_str = event.model_dump_json()
try:
rows = await conn.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled, http_config::text
FROM {webhook_table}
WHERE (bank_id = $1 OR bank_id IS NULL) AND enabled = true
""",
event.bank_id,
)
db_webhooks = [
WebhookConfig(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=row["secret"],
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=_parse_http_config(row["http_config"]),
)
for row in rows
]
all_webhooks = self._global_webhooks + db_webhooks
matched = 0
for webhook in all_webhooks:
if not webhook.enabled:
continue
if event.event.value not in webhook.event_types:
continue
operation_id = uuid.uuid4()
webhook_id = webhook.id if webhook.id else None
task_payload = json.dumps(
{
"type": "webhook_delivery",
"operation_id": str(operation_id),
"bank_id": event.bank_id,
"url": webhook.url,
"secret": webhook.secret,
"event_type": event.event.value,
"payload": payload_str,
"webhook_id": webhook_id,
"http_config": webhook.http_config.model_dump(),
}
)
await conn.execute(
f"""
INSERT INTO {ops_table}
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'pending', $3::jsonb, '{{}}'::jsonb, $4, $4)
""",
operation_id,
event.bank_id,
task_payload,
now,
)
matched += 1
logger.debug(
f"Fired webhook event {event.event} for bank {event.bank_id}: {matched} delivery(ies) queued (in-transaction)"
)
except Exception as e:
logger.error(f"Failed to queue webhook deliveries (in-transaction) for event {event.event}: {e}")
@@ -0,0 +1,51 @@
"""Pydantic models for the webhook system."""
from datetime import datetime
from enum import StrEnum
from pydantic import BaseModel, Field
class WebhookEventType(StrEnum):
CONSOLIDATION_COMPLETED = "consolidation.completed"
RETAIN_COMPLETED = "retain.completed"
class ConsolidationEventData(BaseModel):
observations_created: int | None = None
observations_updated: int | None = None
observations_deleted: int | None = None
error_message: str | None = None
class RetainEventData(BaseModel):
document_id: str | None = None
tags: list[str] | None = None
class WebhookEvent(BaseModel):
event: WebhookEventType
bank_id: str
operation_id: str
status: str # "completed" or "failed"
timestamp: datetime
data: ConsolidationEventData | RetainEventData
class WebhookHttpConfig(BaseModel):
"""HTTP delivery configuration for a webhook."""
method: str = Field(default="POST", description="HTTP method: GET or POST")
timeout_seconds: int = Field(default=30, description="HTTP request timeout in seconds")
headers: dict[str, str] = Field(default_factory=dict, description="Custom HTTP headers")
params: dict[str, str] = Field(default_factory=dict, description="Custom HTTP query parameters")
class WebhookConfig(BaseModel):
id: str
bank_id: str | None
url: str
secret: str | None
event_types: list[str]
enabled: bool
http_config: WebhookHttpConfig = Field(default_factory=WebhookHttpConfig)
@@ -0,0 +1,9 @@
from datetime import datetime
class RetryTaskAt(Exception):
"""Raise from a task handler to schedule a retry at a specific time."""
def __init__(self, retry_at: datetime, message: str = ""):
self.retry_at = retry_at
super().__init__(message)
@@ -219,7 +219,6 @@ def main():
worker_id=args.worker_id,
executor=memory.execute_task,
poll_interval_ms=args.poll_interval,
max_retries=args.max_retries,
schema=schema,
tenant_extension=tenant_extension,
max_slots=config.worker_max_slots,
+38 -49
View File
@@ -14,6 +14,8 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from .exceptions import RetryTaskAt
if TYPE_CHECKING:
import asyncpg
@@ -57,7 +59,6 @@ class WorkerPoller:
worker_id: str,
executor: Callable[[dict[str, Any]], Awaitable[None]],
poll_interval_ms: int = 500,
max_retries: int = 3,
schema: str | None = None,
tenant_extension: "TenantExtension | None" = None,
max_slots: int = 10,
@@ -71,7 +72,6 @@ class WorkerPoller:
worker_id: Unique identifier for this worker
executor: Async function to execute tasks (typically MemoryEngine.execute_task)
poll_interval_ms: Interval between polls when no tasks found (milliseconds)
max_retries: Maximum retry attempts before marking task as failed
schema: Database schema for single-tenant support (deprecated, use tenant_extension)
tenant_extension: Extension for dynamic multi-tenant discovery. If None, creates a
DefaultTenantExtension with the configured schema.
@@ -82,7 +82,6 @@ class WorkerPoller:
self._worker_id = worker_id
self._executor = executor
self._poll_interval_ms = poll_interval_ms
self._max_retries = max_retries
self._schema = schema
# Always set tenant extension (use DefaultTenantExtension if none provided)
if tenant_extension is None:
@@ -218,11 +217,12 @@ class WorkerPoller:
# 1. Claim non-consolidation tasks (up to limit)
non_consolidation_rows = await conn.fetch(
f"""
SELECT operation_id, task_payload
SELECT operation_id, task_payload, retry_count
FROM {table}
WHERE status = 'pending'
AND task_payload IS NOT NULL
AND operation_type != 'consolidation'
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
ORDER BY created_at
LIMIT $1
FOR UPDATE SKIP LOCKED
@@ -238,11 +238,12 @@ class WorkerPoller:
if consolidation_limit > 0 and remaining_limit > 0:
consolidation_rows = await conn.fetch(
f"""
SELECT operation_id, task_payload
SELECT operation_id, task_payload, retry_count
FROM {table} AS pending
WHERE status = 'pending'
AND task_payload IS NOT NULL
AND operation_type = 'consolidation'
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
AND NOT EXISTS (
SELECT 1 FROM {table} AS processing
WHERE processing.bank_id = pending.bank_id
@@ -274,14 +275,19 @@ class WorkerPoller:
)
# Parse and return task payloads with schema context
return [
ClaimedTask(
operation_id=str(row["operation_id"]),
task_dict=json.loads(row["task_payload"]),
schema=schema,
result = []
for row in all_rows:
task_dict = json.loads(row["task_payload"])
task_dict["_retry_count"] = row["retry_count"]
task_dict["_operation_id"] = str(row["operation_id"])
result.append(
ClaimedTask(
operation_id=str(row["operation_id"]),
task_dict=task_dict,
schema=schema,
)
)
for row in all_rows
]
return result
async def _mark_completed(self, operation_id: str, schema: str | None):
"""Mark a task as completed."""
@@ -310,40 +316,22 @@ class WorkerPoller:
error_message,
)
async def _retry_or_fail(self, operation_id: str, error_message: str, schema: str | None):
"""Increment retry count or mark as failed if max retries exceeded."""
async def _schedule_retry(self, operation_id: str, retry_at: "Any", error_message: str, schema: str | None):
"""Reset task to pending with a future retry timestamp."""
table = fq_table("async_operations", schema)
# Get current retry count
row = await self._pool.fetchrow(
f"SELECT retry_count FROM {table} WHERE operation_id = $1",
error_message = error_message[:5000] if len(error_message) > 5000 else error_message
await self._pool.execute(
f"""
UPDATE {table}
SET status = 'pending', next_retry_at = $2, worker_id = NULL, claimed_at = NULL,
retry_count = retry_count + 1, error_message = $3, updated_at = now()
WHERE operation_id = $1
""",
operation_id,
retry_at,
error_message,
)
if row is None:
logger.warning(f"Operation {operation_id} not found, cannot retry")
return
retry_count = row["retry_count"]
if retry_count >= self._max_retries:
# Max retries exceeded, mark as failed
await self._mark_failed(
operation_id, f"Max retries ({self._max_retries}) exceeded. Last error: {error_message}", schema
)
logger.error(f"Task {operation_id} failed after {retry_count} retries")
else:
# Increment retry and reset to pending
await self._pool.execute(
f"""
UPDATE {table}
SET status = 'pending', worker_id = NULL, claimed_at = NULL,
retry_count = retry_count + 1, updated_at = now()
WHERE operation_id = $1
""",
operation_id,
)
logger.warning(f"Task {operation_id} failed, will retry (attempt {retry_count + 1}/{self._max_retries})")
logger.warning(f"Task {operation_id} scheduled for retry at {retry_at}: {error_message}")
async def execute_task(self, task: ClaimedTask):
"""Execute a single task as a background job (fire-and-forget)."""
@@ -378,11 +366,10 @@ class WorkerPoller:
async def _execute_task_inner(self, task: ClaimedTask):
"""Inner task execution with retry/fail handling.
Retryable task failures are re-raised by the executor (MemoryEngine.execute_task)
and handled here via _retry_or_fail, which resets status='pending' (or marks as
'failed' after max retries). Non-retryable failures (e.g., file_convert_retain) are
handled by the executor internally — it marks the operation as failed and returns
normally, so no exception reaches here.
Tasks that want to be retried raise RetryTaskAt; the poller sets next_retry_at
and resets status to 'pending'. All other exceptions are marked as failed immediately.
Non-retryable failures (e.g., file_convert_retain) are handled by the executor
internally — it marks the operation as failed and returns normally.
"""
task_type = task.task_dict.get("type", "unknown")
bank_id = task.task_dict.get("bank_id", "unknown")
@@ -394,10 +381,12 @@ class WorkerPoller:
task.task_dict["_schema"] = task.schema
await self._executor(task.task_dict)
logger.debug(f"Task {task.operation_id} execution finished")
except RetryTaskAt as e:
await self._schedule_retry(task.operation_id, e.retry_at, str(e), task.schema)
except Exception as e:
logger.error(f"Task {task.operation_id} failed: {e}")
traceback.print_exc()
await self._retry_or_fail(task.operation_id, str(e), task.schema)
await self._mark_failed(task.operation_id, str(e), task.schema)
async def recover_own_tasks(self) -> int:
"""
-1
View File
@@ -413,7 +413,6 @@ async def test_worker_batch_recovery(memory, request_context):
worker_id="test_worker_recovery",
executor=memory,
poll_interval_ms=100,
max_retries=3,
schema=schema,
tenant_extension=tenant_extension,
max_slots=5,
+780
View File
@@ -0,0 +1,780 @@
"""Tests for the webhook system.
Covers:
- Unit tests for HMAC signing and retry constants (no DB required)
- Integration tests for fire_event() using a real DB (inserts into async_operations)
- Integration tests for _handle_webhook_delivery() on the memory engine
- HTTP API integration tests for CRUD and delivery listing endpoints
"""
import json
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import pytest_asyncio
from hindsight_api.api import create_app
from hindsight_api.engine.memory_engine import MemoryEngine
from hindsight_api.webhooks.manager import MAX_ATTEMPTS, RETRY_DELAYS, WebhookManager
from hindsight_api.webhooks.models import (
ConsolidationEventData,
RetainEventData,
WebhookConfig,
WebhookEvent,
WebhookEventType,
)
from hindsight_api.worker.exceptions import RetryTaskAt
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_event(bank_id: str = "bank-1") -> WebhookEvent:
return WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=uuid.uuid4().hex,
status="completed",
timestamp=datetime.now(timezone.utc),
data=ConsolidationEventData(observations_created=1),
)
def _make_delivery_task(
bank_id: str = "bank-1",
url: str = "https://example.com/hook",
retry_count: int = 0,
webhook_id: str | None = None,
) -> dict:
return {
"type": "webhook_delivery",
"bank_id": bank_id,
"url": url,
"secret": None,
"event_type": "consolidation.completed",
"payload": '{"event":"consolidation.completed"}',
"webhook_id": webhook_id,
"_retry_count": retry_count,
}
# ---------------------------------------------------------------------------
# Unit tests (no DB)
# ---------------------------------------------------------------------------
class TestHmacSigning:
"""Unit tests for WebhookManager._sign_payload()."""
def _make_manager(self) -> WebhookManager:
"""Create a WebhookManager with a dummy pool (not used for signing)."""
pool = MagicMock()
return WebhookManager(pool=pool, global_webhooks=[])
def test_hmac_signing_format(self):
"""_sign_payload should return a string starting with 'sha256='."""
manager = self._make_manager()
sig = manager._sign_payload("my-secret", b"hello world")
assert sig.startswith("sha256="), f"Expected 'sha256=' prefix, got: {sig!r}"
hex_part = sig[len("sha256="):]
# SHA-256 hex digest is always 64 characters
assert len(hex_part) == 64
# Hex characters only
assert all(c in "0123456789abcdef" for c in hex_part)
def test_hmac_signing_is_deterministic(self):
"""Same secret + payload always produces the same signature."""
manager = self._make_manager()
payload = b'{"event":"consolidation.completed"}'
sig1 = manager._sign_payload("secret-key", payload)
sig2 = manager._sign_payload("secret-key", payload)
assert sig1 == sig2
def test_hmac_signing_differs_with_different_secret(self):
"""Different secrets must produce different signatures."""
manager = self._make_manager()
payload = b"payload"
sig1 = manager._sign_payload("secret-a", payload)
sig2 = manager._sign_payload("secret-b", payload)
assert sig1 != sig2
def test_hmac_signing_differs_with_different_payload(self):
"""Different payloads must produce different signatures."""
manager = self._make_manager()
sig1 = manager._sign_payload("secret", b"payload-one")
sig2 = manager._sign_payload("secret", b"payload-two")
assert sig1 != sig2
class TestRetryConstants:
"""Unit tests to verify retry schedule constants."""
def test_retry_delays_values(self):
"""RETRY_DELAYS must match the documented schedule."""
assert RETRY_DELAYS == [5, 300, 1800, 7200, 18000]
def test_max_attempts(self):
"""MAX_ATTEMPTS should be len(RETRY_DELAYS) + 1."""
assert MAX_ATTEMPTS == 6
assert MAX_ATTEMPTS == len(RETRY_DELAYS) + 1
# ---------------------------------------------------------------------------
# DB integration tests
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def webhook_manager(memory: MemoryEngine) -> WebhookManager:
"""Return a WebhookManager backed by the test pool with no global webhooks."""
return WebhookManager(pool=memory._pool, global_webhooks=[])
class TestFireEvent:
"""Integration tests for WebhookManager.fire_event()."""
@pytest.mark.asyncio
async def test_fire_event_creates_delivery(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""fire_event() inserts a pending webhook_delivery task in async_operations."""
bank_id = f"wh-test-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/hook",
["consolidation.completed"],
)
try:
event = _make_event(bank_id)
await webhook_manager.fire_event(event)
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT status, task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
""",
bank_id,
str(webhook_id),
)
assert len(rows) == 1
assert rows[0]["status"] == "pending"
payload = rows[0]["task_payload"]
if isinstance(payload, str):
payload = json.loads(payload)
assert payload["event_type"] == "consolidation.completed"
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)
@pytest.mark.asyncio
async def test_fire_event_global_webhook(
self, memory: MemoryEngine
):
"""fire_event() also queues delivery tasks for global webhooks (not stored in DB)."""
bank_id = f"wh-global-{uuid.uuid4().hex[:8]}"
global_webhook = WebhookConfig(
id="", # No DB row
bank_id=None,
url="https://global.example.com/hook",
secret=None,
event_types=["consolidation.completed"],
enabled=True,
)
manager = WebhookManager(pool=memory._pool, global_webhooks=[global_webhook])
event = _make_event(bank_id)
await manager.fire_event(event)
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT status, task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'url' = 'https://global.example.com/hook'
ORDER BY created_at DESC
LIMIT 1
"""
,
bank_id,
)
assert len(rows) == 1
assert rows[0]["status"] == "pending"
payload = rows[0]["task_payload"]
if isinstance(payload, str):
payload = json.loads(payload)
assert payload["webhook_id"] is None # global webhook has no DB row
# Cleanup
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
@pytest.mark.asyncio
async def test_fire_event_no_match_if_event_type_mismatch(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""Webhooks registered for a different event type receive no delivery task."""
bank_id = f"wh-mismatch-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/other-hook",
["other.event"],
)
try:
event = _make_event(bank_id)
await webhook_manager.fire_event(event)
async with memory._pool.acquire() as conn:
count = await conn.fetchval(
"""
SELECT COUNT(*) FROM async_operations
WHERE operation_type = 'webhook_delivery' AND bank_id = $1
""",
bank_id,
)
assert count == 0
finally:
async with memory._pool.acquire() as conn:
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)
class TestHandleWebhookDelivery:
"""Integration tests for MemoryEngine._handle_webhook_delivery()."""
@pytest.mark.asyncio
async def test_deliver_success(self, memory: MemoryEngine):
"""A successful HTTP POST completes without raising."""
task_dict = _make_delivery_task(retry_count=0)
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
with patch.object(memory._http_client, "post", new=AsyncMock(return_value=mock_response)):
# Should not raise
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_failure_raises_retry_task_at(self, memory: MemoryEngine):
"""A failed HTTP POST raises RetryTaskAt when retries remain."""
task_dict = _make_delivery_task(retry_count=0)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("connection refused"))
):
with pytest.raises(RetryTaskAt):
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_exhausted_retries_raises(self, memory: MemoryEngine):
"""When retry_count reaches MAX_ATTEMPTS-1, a failure raises the original exception."""
task_dict = _make_delivery_task(retry_count=MAX_ATTEMPTS - 1)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("server error"))
):
with pytest.raises(Exception, match="server error"):
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_retry_at_uses_delay_schedule(self, memory: MemoryEngine):
"""RetryTaskAt.retry_at is approximately now + RETRY_DELAYS[retry_count]."""
from datetime import timedelta
task_dict = _make_delivery_task(retry_count=1)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("fail"))
):
before = datetime.now(timezone.utc)
with pytest.raises(RetryTaskAt) as exc_info:
await memory._handle_webhook_delivery(task_dict)
after = datetime.now(timezone.utc)
retry_at = exc_info.value.retry_at
expected_delay = RETRY_DELAYS[1] # retry_count=1
assert retry_at >= before + timedelta(seconds=expected_delay - 2)
assert retry_at <= after + timedelta(seconds=expected_delay + 2)
@pytest.mark.asyncio
async def test_execute_task_marks_operation_completed(self, memory: MemoryEngine):
"""After a successful delivery, execute_task marks the async_operations row as completed."""
operation_id = str(uuid.uuid4())
bank_id = f"wh-exec-{uuid.uuid4().hex[:8]}"
# Insert a real async_operations row so _mark_operation_completed has something to update
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO async_operations
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'processing', '{}'::jsonb, '{}'::jsonb, NOW(), NOW())
""",
uuid.UUID(operation_id),
bank_id,
)
task_dict = {
**_make_delivery_task(bank_id=bank_id, retry_count=0),
"operation_id": operation_id,
}
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
with patch.object(memory._http_client, "post", new=AsyncMock(return_value=mock_response)):
await memory.execute_task(task_dict)
async with memory._pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT status FROM async_operations WHERE operation_id = $1",
uuid.UUID(operation_id),
)
assert row is not None
assert row["status"] == "completed", f"Expected 'completed', got '{row['status']}'"
# Cleanup
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_id = $1",
uuid.UUID(operation_id),
)
# ---------------------------------------------------------------------------
# HTTP API integration tests
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def api_client(memory: MemoryEngine):
"""Async HTTP test client wired to the FastAPI app."""
app = create_app(memory, initialize_memory=False)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
yield client
class TestWebhookHttpApi:
"""HTTP API integration tests for webhook CRUD endpoints."""
@pytest.mark.asyncio
async def test_http_create_webhook(self, api_client: httpx.AsyncClient):
"""POST /webhooks returns 201 and an id."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
response = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={
"url": "https://example.com/create",
"event_types": ["consolidation.completed"],
},
)
assert response.status_code == 201, response.text
data = response.json()
assert "id" in data
assert data["url"] == "https://example.com/create"
assert data["bank_id"] == bank_id
assert data["secret"] is None # secrets are never echoed back
# Cleanup
await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{data['id']}"
)
@pytest.mark.asyncio
async def test_http_list_webhooks(self, api_client: httpx.AsyncClient):
"""GET /webhooks returns the webhooks registered for a bank."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/list", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
list_resp = await api_client.get(f"/v1/default/banks/{bank_id}/webhooks")
assert list_resp.status_code == 200
items = list_resp.json()["items"]
assert any(item["id"] == webhook_id for item in items)
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_delete_webhook(self, api_client: httpx.AsyncClient):
"""DELETE /webhooks/{id} removes the webhook; subsequent list returns empty for that bank."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/delete", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
delete_resp = await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
)
assert delete_resp.status_code == 200
assert delete_resp.json()["success"] is True
list_resp = await api_client.get(f"/v1/default/banks/{bank_id}/webhooks")
assert list_resp.status_code == 200
ids = [item["id"] for item in list_resp.json()["items"]]
assert webhook_id not in ids
@pytest.mark.asyncio
async def test_http_delete_webhook_not_found(self, api_client: httpx.AsyncClient):
"""DELETE with a non-existent webhook id returns 404."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_list_deliveries(
self, memory: MemoryEngine, api_client: httpx.AsyncClient
):
"""GET /webhooks/{id}/deliveries returns delivery records for a webhook."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
# Create webhook via HTTP API
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={
"url": "https://example.com/deliveries",
"event_types": ["consolidation.completed"],
},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
# Insert a delivery row directly into async_operations
delivery_id = uuid.uuid4()
now = datetime.now(timezone.utc)
task_payload = json.dumps(
{
"type": "webhook_delivery",
"bank_id": bank_id,
"url": "https://example.com/deliveries",
"secret": None,
"event_type": "consolidation.completed",
"payload": '{"event":"consolidation.completed"}',
"webhook_id": webhook_id,
}
)
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO async_operations
(operation_id, bank_id, operation_type, status, retry_count, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'completed', 0, $3::jsonb, '{}'::jsonb, $4, $4)
""",
delivery_id,
bank_id,
task_payload,
now,
)
try:
deliveries_resp = await api_client.get(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries"
)
assert deliveries_resp.status_code == 200
items = deliveries_resp.json()["items"]
ids = [item["id"] for item in items]
assert str(delivery_id) in ids
# Verify shape of a delivery item
delivery = next(item for item in items if item["id"] == str(delivery_id))
assert delivery["status"] == "completed"
assert delivery["event_type"] == "consolidation.completed"
assert delivery["attempts"] == 1
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_id = $1", delivery_id
)
await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
)
@pytest.mark.asyncio
async def test_http_list_deliveries_webhook_not_found(self, api_client: httpx.AsyncClient):
"""GET /webhooks/{id}/deliveries for a non-existent webhook returns 404."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.get(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}/deliveries"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_update_webhook_url(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} updates only the provided fields."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/original", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"url": "https://example.com/updated"},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["url"] == "https://example.com/updated"
# event_types should be unchanged
assert "consolidation.completed" in data["event_types"]
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_event_types(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can update event_types."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"event_types": ["retain.completed"]},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["event_types"] == ["retain.completed"]
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_enabled(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can toggle enabled."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
assert create_resp.json()["enabled"] is True
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"enabled": False},
)
assert patch_resp.status_code == 200
assert patch_resp.json()["enabled"] is False
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_http_config(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can update http_config."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={
"http_config": {
"method": "POST",
"timeout_seconds": 10,
"headers": {"X-Custom": "value"},
"params": {},
}
},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["http_config"]["timeout_seconds"] == 10
assert data["http_config"]["headers"] == {"X-Custom": "value"}
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_not_found(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} returns 404 for a non-existent webhook."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}",
json={"url": "https://example.com/new"},
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_update_webhook_no_fields(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} with empty body returns 422."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={},
)
assert patch_resp.status_code == 422
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
# ---------------------------------------------------------------------------
# retain.completed webhook tests
# ---------------------------------------------------------------------------
class TestRetainCompletedWebhook:
"""Tests for the retain.completed webhook event."""
def test_retain_event_data_model(self):
"""RetainEventData can be constructed with optional fields."""
data = RetainEventData(document_id="doc-123", tags=["tag1", "tag2"])
assert data.document_id == "doc-123"
assert data.tags == ["tag1", "tag2"]
empty = RetainEventData()
assert empty.document_id is None
assert empty.tags is None
def test_retain_event_type_value(self):
"""WebhookEventType.RETAIN_COMPLETED has the correct string value."""
assert WebhookEventType.RETAIN_COMPLETED == "retain.completed"
@pytest.mark.asyncio
async def test_fire_retain_webhook_queues_per_document(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""_fire_retain_webhook queues one delivery task per content item."""
bank_id = f"wh-retain-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/retain-hook",
["retain.completed"],
)
try:
contents = [
{"content": "Alice works at Google", "document_id": "doc-1"},
{"content": "Bob loves Python", "document_id": "doc-2"},
]
# Temporarily replace webhook manager on memory engine
original_manager = memory._webhook_manager
memory._webhook_manager = webhook_manager
try:
callback = memory._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id="test-op-123",
)
assert callback is not None
async with memory._pool.acquire() as conn:
await callback(conn)
finally:
memory._webhook_manager = original_manager
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'event_type' = 'retain.completed'
ORDER BY created_at
""",
bank_id,
)
assert len(rows) == 2
payloads = []
for row in rows:
p = row["task_payload"]
if isinstance(p, str):
p = json.loads(p)
payloads.append(p)
doc_ids_in_payloads = [json.loads(p["payload"]).get("data", {}).get("document_id") for p in payloads]
assert "doc-1" in doc_ids_in_payloads
assert "doc-2" in doc_ids_in_payloads
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)
+13 -15
View File
@@ -294,14 +294,17 @@ class TestWorkerPoller:
payload,
)
from datetime import datetime, timezone
from hindsight_api.worker.exceptions import RetryTaskAt
async def failing_executor(task_dict):
raise ValueError("TimeoutError during recall")
raise RetryTaskAt(retry_at=datetime.now(timezone.utc), message="TimeoutError during recall")
poller = WorkerPoller(
pool=pool,
worker_id="test-worker-1",
executor=failing_executor,
max_retries=3,
)
task_dict = json.loads(payload)
@@ -326,39 +329,35 @@ class TestWorkerPoller:
assert row["retry_count"] == 1
@pytest.mark.asyncio
async def test_executor_exception_marks_failed_after_max_retries(self, pool, clean_operations):
"""Test that a task is permanently marked 'failed' once retry_count hits max_retries.
async def test_executor_exception_marks_failed_immediately(self, pool, clean_operations):
"""Test that a plain exception (not RetryTaskAt) permanently marks a task as 'failed'.
After max_retries exhaustion the task must NOT be reset to 'pending' — it should
be marked 'failed' with an error message so it stops consuming retry budget.
With the task-owned retry model, plain exceptions are non-retryable — the poller
marks them as 'failed' immediately. Tasks that want to be retried must raise RetryTaskAt.
"""
from hindsight_api.worker import WorkerPoller
from hindsight_api.worker.poller import ClaimedTask
max_retries = 3
bank_id = f"test-worker-{uuid.uuid4().hex[:8]}"
op_id = uuid.uuid4()
payload = json.dumps({"type": "consolidation", "operation_id": str(op_id), "bank_id": bank_id})
# Insert with retry_count already at the limit
await pool.execute(
"""
INSERT INTO async_operations (operation_id, bank_id, operation_type, status, task_payload, worker_id, claimed_at, retry_count)
VALUES ($1, $2, 'consolidation', 'processing', $3::jsonb, 'test-worker-1', now(), $4)
VALUES ($1, $2, 'consolidation', 'processing', $3::jsonb, 'test-worker-1', now(), 0)
""",
op_id,
bank_id,
payload,
max_retries,
)
async def failing_executor(task_dict):
raise ValueError("Still failing after all retries")
raise ValueError("Non-retryable error")
poller = WorkerPoller(
pool=pool,
worker_id="test-worker-1",
executor=failing_executor,
max_retries=max_retries,
)
task_dict = json.loads(payload)
@@ -373,11 +372,10 @@ class TestWorkerPoller:
op_id,
)
assert row["status"] == "failed", (
f"Expected 'failed' after max retries, got '{row['status']}'"
f"Expected 'failed' for plain exception, got '{row['status']}'"
)
assert row["error_message"] is not None
assert "Max retries" in row["error_message"]
assert row["retry_count"] == max_retries # not incremented further
assert row["retry_count"] == 0 # not incremented; plain exception = immediate fail
@pytest.mark.asyncio
async def test_executor_failed_status_not_overridden(self, pool, clean_operations):
+556
View File
@@ -2111,6 +2111,248 @@ paths:
summary: Trigger consolidation
tags:
- Banks
/v1/default/banks/{bank_id}/webhooks:
get:
description: List all webhooks registered for a bank.
operationId: list_webhooks
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookListResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: List webhooks
tags:
- Webhooks
post:
description: Register a webhook endpoint to receive event notifications for
this bank.
operationId: create_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWebhookRequest'
required: true
responses:
"201":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Register webhook
tags:
- Webhooks
/v1/default/banks/{bank_id}/webhooks/{webhook_id}:
delete:
description: Remove a registered webhook.
operationId: delete_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Delete webhook
tags:
- Webhooks
patch:
description: Update one or more fields of a registered webhook. Only provided
fields are changed.
operationId: update_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWebhookRequest'
required: true
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Update webhook
tags:
- Webhooks
/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries:
get:
description: Inspect delivery history for a webhook (useful for debugging).
operationId: list_webhook_deliveries
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- description: Maximum number of deliveries to return
explode: true
in: query
name: limit
required: false
schema:
default: 50
description: Maximum number of deliveries to return
maximum: 200
title: Limit
type: integer
style: form
- description: Pagination cursor (created_at of last item)
explode: true
in: query
name: cursor
required: false
schema:
nullable: true
type: string
style: form
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookDeliveryListResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: List webhook deliveries
tags:
- Webhooks
/v1/default/banks/{bank_id}/memories:
delete:
description: "Delete memory units for a memory bank. Optionally filter by type\
@@ -2879,6 +3121,47 @@ components:
required:
- operation_id
title: CreateMentalModelResponse
CreateWebhookRequest:
description: Request model for registering a webhook.
example:
event_types:
- event_types
- event_types
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
url:
description: HTTP(S) endpoint URL to deliver events to
title: Url
type: string
secret:
nullable: true
type: string
event_types:
default:
- consolidation.completed
description: "List of event types to deliver. Currently supported: 'consolidation.completed'"
items:
type: string
type: array
enabled:
default: true
description: Whether this webhook is active
title: Enabled
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
required:
- url
title: CreateWebhookRequest
DeleteDocumentResponse:
description: Response model for delete document endpoint.
example:
@@ -4437,6 +4720,41 @@ components:
trigger:
$ref: '#/components/schemas/MentalModelTrigger'
title: UpdateMentalModelRequest
UpdateWebhookRequest:
description: Request model for updating a webhook. Only provided fields are
updated.
example:
event_types:
- event_types
- event_types
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
url:
nullable: true
type: string
secret:
nullable: true
type: string
event_types:
items:
type: string
nullable: true
type: array
enabled:
nullable: true
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
title: UpdateWebhookRequest
ValidationError:
example:
msg: msg
@@ -4481,6 +4799,244 @@ components:
- api_version
- features
title: VersionResponse
WebhookDeliveryListResponse:
description: Response model for listing webhook deliveries.
example:
next_cursor: next_cursor
items:
- last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
- last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
properties:
items:
items:
$ref: '#/components/schemas/WebhookDeliveryResponse'
type: array
next_cursor:
nullable: true
type: string
required:
- items
title: WebhookDeliveryListResponse
WebhookDeliveryResponse:
description: Response model for a webhook delivery record.
example:
last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
properties:
id:
title: Id
type: string
webhook_id:
nullable: true
type: string
url:
title: Url
type: string
event_type:
title: Event Type
type: string
status:
title: Status
type: string
attempts:
title: Attempts
type: integer
next_retry_at:
nullable: true
type: string
last_error:
nullable: true
type: string
last_response_status:
nullable: true
type: integer
last_response_body:
nullable: true
type: string
last_attempt_at:
nullable: true
type: string
created_at:
nullable: true
type: string
updated_at:
nullable: true
type: string
required:
- attempts
- event_type
- id
- status
- url
- webhook_id
title: WebhookDeliveryResponse
WebhookHttpConfig:
description: HTTP delivery configuration for a webhook.
example:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
properties:
method:
default: POST
description: "HTTP method: GET or POST"
title: Method
type: string
timeout_seconds:
default: 30
description: HTTP request timeout in seconds
title: Timeout Seconds
type: integer
headers:
additionalProperties:
type: string
description: Custom HTTP headers
title: Headers
params:
additionalProperties:
type: string
description: Custom HTTP query parameters
title: Params
title: WebhookHttpConfig
WebhookListResponse:
description: Response model for listing webhooks.
example:
items:
- event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
- event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
items:
items:
$ref: '#/components/schemas/WebhookResponse'
type: array
required:
- items
title: WebhookListResponse
WebhookResponse:
description: Response model for a webhook.
example:
event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
id:
title: Id
type: string
bank_id:
nullable: true
type: string
url:
title: Url
type: string
secret:
nullable: true
type: string
event_types:
items:
type: string
type: array
enabled:
title: Enabled
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
created_at:
nullable: true
type: string
updated_at:
nullable: true
type: string
required:
- bank_id
- enabled
- event_types
- id
- url
title: WebhookResponse
Timestamp:
anyOf:
- format: date-time
+691
View File
@@ -0,0 +1,691 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"strings"
)
// WebhooksAPIService WebhooksAPI service
type WebhooksAPIService service
type ApiCreateWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
createWebhookRequest *CreateWebhookRequest
authorization *string
}
func (r ApiCreateWebhookRequest) CreateWebhookRequest(createWebhookRequest CreateWebhookRequest) ApiCreateWebhookRequest {
r.createWebhookRequest = &createWebhookRequest
return r
}
func (r ApiCreateWebhookRequest) Authorization(authorization string) ApiCreateWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiCreateWebhookRequest) Execute() (*WebhookResponse, *http.Response, error) {
return r.ApiService.CreateWebhookExecute(r)
}
/*
CreateWebhook Register webhook
Register a webhook endpoint to receive event notifications for this bank.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiCreateWebhookRequest
*/
func (a *WebhooksAPIService) CreateWebhook(ctx context.Context, bankId string) ApiCreateWebhookRequest {
return ApiCreateWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
}
}
// Execute executes the request
// @return WebhookResponse
func (a *WebhooksAPIService) CreateWebhookExecute(r ApiCreateWebhookRequest) (*WebhookResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.CreateWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.createWebhookRequest == nil {
return localVarReturnValue, nil, reportError("createWebhookRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
// body params
localVarPostBody = r.createWebhookRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiDeleteWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
authorization *string
}
func (r ApiDeleteWebhookRequest) Authorization(authorization string) ApiDeleteWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiDeleteWebhookRequest) Execute() (*DeleteResponse, *http.Response, error) {
return r.ApiService.DeleteWebhookExecute(r)
}
/*
DeleteWebhook Delete webhook
Remove a registered webhook.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiDeleteWebhookRequest
*/
func (a *WebhooksAPIService) DeleteWebhook(ctx context.Context, bankId string, webhookId string) ApiDeleteWebhookRequest {
return ApiDeleteWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return DeleteResponse
func (a *WebhooksAPIService) DeleteWebhookExecute(r ApiDeleteWebhookRequest) (*DeleteResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodDelete
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *DeleteResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.DeleteWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiListWebhookDeliveriesRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
limit *int32
cursor *string
authorization *string
}
// Maximum number of deliveries to return
func (r ApiListWebhookDeliveriesRequest) Limit(limit int32) ApiListWebhookDeliveriesRequest {
r.limit = &limit
return r
}
// Pagination cursor (created_at of last item)
func (r ApiListWebhookDeliveriesRequest) Cursor(cursor string) ApiListWebhookDeliveriesRequest {
r.cursor = &cursor
return r
}
func (r ApiListWebhookDeliveriesRequest) Authorization(authorization string) ApiListWebhookDeliveriesRequest {
r.authorization = &authorization
return r
}
func (r ApiListWebhookDeliveriesRequest) Execute() (*WebhookDeliveryListResponse, *http.Response, error) {
return r.ApiService.ListWebhookDeliveriesExecute(r)
}
/*
ListWebhookDeliveries List webhook deliveries
Inspect delivery history for a webhook (useful for debugging).
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiListWebhookDeliveriesRequest
*/
func (a *WebhooksAPIService) ListWebhookDeliveries(ctx context.Context, bankId string, webhookId string) ApiListWebhookDeliveriesRequest {
return ApiListWebhookDeliveriesRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return WebhookDeliveryListResponse
func (a *WebhooksAPIService) ListWebhookDeliveriesExecute(r ApiListWebhookDeliveriesRequest) (*WebhookDeliveryListResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookDeliveryListResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.ListWebhookDeliveries")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.limit != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "")
} else {
var defaultValue int32 = 50
r.limit = &defaultValue
}
if r.cursor != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "cursor", r.cursor, "form", "")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiListWebhooksRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
authorization *string
}
func (r ApiListWebhooksRequest) Authorization(authorization string) ApiListWebhooksRequest {
r.authorization = &authorization
return r
}
func (r ApiListWebhooksRequest) Execute() (*WebhookListResponse, *http.Response, error) {
return r.ApiService.ListWebhooksExecute(r)
}
/*
ListWebhooks List webhooks
List all webhooks registered for a bank.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiListWebhooksRequest
*/
func (a *WebhooksAPIService) ListWebhooks(ctx context.Context, bankId string) ApiListWebhooksRequest {
return ApiListWebhooksRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
}
}
// Execute executes the request
// @return WebhookListResponse
func (a *WebhooksAPIService) ListWebhooksExecute(r ApiListWebhooksRequest) (*WebhookListResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookListResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.ListWebhooks")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiUpdateWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
updateWebhookRequest *UpdateWebhookRequest
authorization *string
}
func (r ApiUpdateWebhookRequest) UpdateWebhookRequest(updateWebhookRequest UpdateWebhookRequest) ApiUpdateWebhookRequest {
r.updateWebhookRequest = &updateWebhookRequest
return r
}
func (r ApiUpdateWebhookRequest) Authorization(authorization string) ApiUpdateWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiUpdateWebhookRequest) Execute() (*WebhookResponse, *http.Response, error) {
return r.ApiService.UpdateWebhookExecute(r)
}
/*
UpdateWebhook Update webhook
Update one or more fields of a registered webhook. Only provided fields are changed.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiUpdateWebhookRequest
*/
func (a *WebhooksAPIService) UpdateWebhook(ctx context.Context, bankId string, webhookId string) ApiUpdateWebhookRequest {
return ApiUpdateWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return WebhookResponse
func (a *WebhooksAPIService) UpdateWebhookExecute(r ApiUpdateWebhookRequest) (*WebhookResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPatch
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.UpdateWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.updateWebhookRequest == nil {
return localVarReturnValue, nil, reportError("updateWebhookRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
// body params
localVarPostBody = r.updateWebhookRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
+3
View File
@@ -66,6 +66,8 @@ type APIClient struct {
MonitoringAPI *MonitoringAPIService
OperationsAPI *OperationsAPIService
WebhooksAPI *WebhooksAPIService
}
type service struct {
@@ -93,6 +95,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
c.MentalModelsAPI = (*MentalModelsAPIService)(&c.common)
c.MonitoringAPI = (*MonitoringAPIService)(&c.common)
c.OperationsAPI = (*OperationsAPIService)(&c.common)
c.WebhooksAPI = (*WebhooksAPIService)(&c.common)
return c
}
@@ -0,0 +1,320 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the CreateWebhookRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &CreateWebhookRequest{}
// CreateWebhookRequest Request model for registering a webhook.
type CreateWebhookRequest struct {
// HTTP(S) endpoint URL to deliver events to
Url string `json:"url"`
Secret NullableString `json:"secret,omitempty"`
// List of event types to deliver. Currently supported: 'consolidation.completed'
EventTypes []string `json:"event_types,omitempty"`
// Whether this webhook is active
Enabled *bool `json:"enabled,omitempty"`
// HTTP delivery configuration (method, timeout, headers, params)
HttpConfig *WebhookHttpConfig `json:"http_config,omitempty"`
}
type _CreateWebhookRequest CreateWebhookRequest
// NewCreateWebhookRequest instantiates a new CreateWebhookRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewCreateWebhookRequest(url string) *CreateWebhookRequest {
this := CreateWebhookRequest{}
this.Url = url
var enabled bool = true
this.Enabled = &enabled
return &this
}
// NewCreateWebhookRequestWithDefaults instantiates a new CreateWebhookRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewCreateWebhookRequestWithDefaults() *CreateWebhookRequest {
this := CreateWebhookRequest{}
var enabled bool = true
this.Enabled = &enabled
return &this
}
// GetUrl returns the Url field value
func (o *CreateWebhookRequest) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *CreateWebhookRequest) SetUrl(v string) {
o.Url = v
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateWebhookRequest) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateWebhookRequest) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *CreateWebhookRequest) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *CreateWebhookRequest) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *CreateWebhookRequest) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetEventTypes() []string {
if o == nil || IsNil(o.EventTypes) {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetEventTypesOk() ([]string, bool) {
if o == nil || IsNil(o.EventTypes) {
return nil, false
}
return o.EventTypes, true
}
// HasEventTypes returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasEventTypes() bool {
if o != nil && !IsNil(o.EventTypes) {
return true
}
return false
}
// SetEventTypes gets a reference to the given []string and assigns it to the EventTypes field.
func (o *CreateWebhookRequest) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetEnabled() bool {
if o == nil || IsNil(o.Enabled) {
var ret bool
return ret
}
return *o.Enabled
}
// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetEnabledOk() (*bool, bool) {
if o == nil || IsNil(o.Enabled) {
return nil, false
}
return o.Enabled, true
}
// HasEnabled returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasEnabled() bool {
if o != nil && !IsNil(o.Enabled) {
return true
}
return false
}
// SetEnabled gets a reference to the given bool and assigns it to the Enabled field.
func (o *CreateWebhookRequest) SetEnabled(v bool) {
o.Enabled = &v
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil || IsNil(o.HttpConfig) {
return nil, false
}
return o.HttpConfig, true
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasHttpConfig() bool {
if o != nil && !IsNil(o.HttpConfig) {
return true
}
return false
}
// SetHttpConfig gets a reference to the given WebhookHttpConfig and assigns it to the HttpConfig field.
func (o *CreateWebhookRequest) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig = &v
}
func (o CreateWebhookRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o CreateWebhookRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["url"] = o.Url
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
if !IsNil(o.EventTypes) {
toSerialize["event_types"] = o.EventTypes
}
if !IsNil(o.Enabled) {
toSerialize["enabled"] = o.Enabled
}
if !IsNil(o.HttpConfig) {
toSerialize["http_config"] = o.HttpConfig
}
return toSerialize, nil
}
func (o *CreateWebhookRequest) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"url",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varCreateWebhookRequest := _CreateWebhookRequest{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varCreateWebhookRequest)
if err != nil {
return err
}
*o = CreateWebhookRequest(varCreateWebhookRequest)
return err
}
type NullableCreateWebhookRequest struct {
value *CreateWebhookRequest
isSet bool
}
func (v NullableCreateWebhookRequest) Get() *CreateWebhookRequest {
return v.value
}
func (v *NullableCreateWebhookRequest) Set(val *CreateWebhookRequest) {
v.value = val
v.isSet = true
}
func (v NullableCreateWebhookRequest) IsSet() bool {
return v.isSet
}
func (v *NullableCreateWebhookRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableCreateWebhookRequest(val *CreateWebhookRequest) *NullableCreateWebhookRequest {
return &NullableCreateWebhookRequest{value: val, isSet: true}
}
func (v NullableCreateWebhookRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableCreateWebhookRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,311 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the UpdateWebhookRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UpdateWebhookRequest{}
// UpdateWebhookRequest Request model for updating a webhook. Only provided fields are updated.
type UpdateWebhookRequest struct {
Url NullableString `json:"url,omitempty"`
Secret NullableString `json:"secret,omitempty"`
EventTypes []string `json:"event_types,omitempty"`
Enabled NullableBool `json:"enabled,omitempty"`
HttpConfig NullableWebhookHttpConfig `json:"http_config,omitempty"`
}
// NewUpdateWebhookRequest instantiates a new UpdateWebhookRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUpdateWebhookRequest() *UpdateWebhookRequest {
this := UpdateWebhookRequest{}
return &this
}
// NewUpdateWebhookRequestWithDefaults instantiates a new UpdateWebhookRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUpdateWebhookRequestWithDefaults() *UpdateWebhookRequest {
this := UpdateWebhookRequest{}
return &this
}
// GetUrl returns the Url field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetUrl() string {
if o == nil || IsNil(o.Url.Get()) {
var ret string
return ret
}
return *o.Url.Get()
}
// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Url.Get(), o.Url.IsSet()
}
// HasUrl returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasUrl() bool {
if o != nil && o.Url.IsSet() {
return true
}
return false
}
// SetUrl gets a reference to the given NullableString and assigns it to the Url field.
func (o *UpdateWebhookRequest) SetUrl(v string) {
o.Url.Set(&v)
}
// SetUrlNil sets the value for Url to be an explicit nil
func (o *UpdateWebhookRequest) SetUrlNil() {
o.Url.Set(nil)
}
// UnsetUrl ensures that no value is present for Url, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetUrl() {
o.Url.Unset()
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *UpdateWebhookRequest) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *UpdateWebhookRequest) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetEventTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetEventTypesOk() ([]string, bool) {
if o == nil || IsNil(o.EventTypes) {
return nil, false
}
return o.EventTypes, true
}
// HasEventTypes returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasEventTypes() bool {
if o != nil && !IsNil(o.EventTypes) {
return true
}
return false
}
// SetEventTypes gets a reference to the given []string and assigns it to the EventTypes field.
func (o *UpdateWebhookRequest) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetEnabled() bool {
if o == nil || IsNil(o.Enabled.Get()) {
var ret bool
return ret
}
return *o.Enabled.Get()
}
// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetEnabledOk() (*bool, bool) {
if o == nil {
return nil, false
}
return o.Enabled.Get(), o.Enabled.IsSet()
}
// HasEnabled returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasEnabled() bool {
if o != nil && o.Enabled.IsSet() {
return true
}
return false
}
// SetEnabled gets a reference to the given NullableBool and assigns it to the Enabled field.
func (o *UpdateWebhookRequest) SetEnabled(v bool) {
o.Enabled.Set(&v)
}
// SetEnabledNil sets the value for Enabled to be an explicit nil
func (o *UpdateWebhookRequest) SetEnabledNil() {
o.Enabled.Set(nil)
}
// UnsetEnabled ensures that no value is present for Enabled, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetEnabled() {
o.Enabled.Unset()
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig.Get()) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig.Get()
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil {
return nil, false
}
return o.HttpConfig.Get(), o.HttpConfig.IsSet()
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasHttpConfig() bool {
if o != nil && o.HttpConfig.IsSet() {
return true
}
return false
}
// SetHttpConfig gets a reference to the given NullableWebhookHttpConfig and assigns it to the HttpConfig field.
func (o *UpdateWebhookRequest) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig.Set(&v)
}
// SetHttpConfigNil sets the value for HttpConfig to be an explicit nil
func (o *UpdateWebhookRequest) SetHttpConfigNil() {
o.HttpConfig.Set(nil)
}
// UnsetHttpConfig ensures that no value is present for HttpConfig, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetHttpConfig() {
o.HttpConfig.Unset()
}
func (o UpdateWebhookRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UpdateWebhookRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if o.Url.IsSet() {
toSerialize["url"] = o.Url.Get()
}
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
if o.EventTypes != nil {
toSerialize["event_types"] = o.EventTypes
}
if o.Enabled.IsSet() {
toSerialize["enabled"] = o.Enabled.Get()
}
if o.HttpConfig.IsSet() {
toSerialize["http_config"] = o.HttpConfig.Get()
}
return toSerialize, nil
}
type NullableUpdateWebhookRequest struct {
value *UpdateWebhookRequest
isSet bool
}
func (v NullableUpdateWebhookRequest) Get() *UpdateWebhookRequest {
return v.value
}
func (v *NullableUpdateWebhookRequest) Set(val *UpdateWebhookRequest) {
v.value = val
v.isSet = true
}
func (v NullableUpdateWebhookRequest) IsSet() bool {
return v.isSet
}
func (v *NullableUpdateWebhookRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUpdateWebhookRequest(val *UpdateWebhookRequest) *NullableUpdateWebhookRequest {
return &NullableUpdateWebhookRequest{value: val, isSet: true}
}
func (v NullableUpdateWebhookRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUpdateWebhookRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,204 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookDeliveryListResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookDeliveryListResponse{}
// WebhookDeliveryListResponse Response model for listing webhook deliveries.
type WebhookDeliveryListResponse struct {
Items []WebhookDeliveryResponse `json:"items"`
NextCursor NullableString `json:"next_cursor,omitempty"`
}
type _WebhookDeliveryListResponse WebhookDeliveryListResponse
// NewWebhookDeliveryListResponse instantiates a new WebhookDeliveryListResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookDeliveryListResponse(items []WebhookDeliveryResponse) *WebhookDeliveryListResponse {
this := WebhookDeliveryListResponse{}
this.Items = items
return &this
}
// NewWebhookDeliveryListResponseWithDefaults instantiates a new WebhookDeliveryListResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookDeliveryListResponseWithDefaults() *WebhookDeliveryListResponse {
this := WebhookDeliveryListResponse{}
return &this
}
// GetItems returns the Items field value
func (o *WebhookDeliveryListResponse) GetItems() []WebhookDeliveryResponse {
if o == nil {
var ret []WebhookDeliveryResponse
return ret
}
return o.Items
}
// GetItemsOk returns a tuple with the Items field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryListResponse) GetItemsOk() ([]WebhookDeliveryResponse, bool) {
if o == nil {
return nil, false
}
return o.Items, true
}
// SetItems sets field value
func (o *WebhookDeliveryListResponse) SetItems(v []WebhookDeliveryResponse) {
o.Items = v
}
// GetNextCursor returns the NextCursor field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryListResponse) GetNextCursor() string {
if o == nil || IsNil(o.NextCursor.Get()) {
var ret string
return ret
}
return *o.NextCursor.Get()
}
// GetNextCursorOk returns a tuple with the NextCursor field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryListResponse) GetNextCursorOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.NextCursor.Get(), o.NextCursor.IsSet()
}
// HasNextCursor returns a boolean if a field has been set.
func (o *WebhookDeliveryListResponse) HasNextCursor() bool {
if o != nil && o.NextCursor.IsSet() {
return true
}
return false
}
// SetNextCursor gets a reference to the given NullableString and assigns it to the NextCursor field.
func (o *WebhookDeliveryListResponse) SetNextCursor(v string) {
o.NextCursor.Set(&v)
}
// SetNextCursorNil sets the value for NextCursor to be an explicit nil
func (o *WebhookDeliveryListResponse) SetNextCursorNil() {
o.NextCursor.Set(nil)
}
// UnsetNextCursor ensures that no value is present for NextCursor, not even an explicit nil
func (o *WebhookDeliveryListResponse) UnsetNextCursor() {
o.NextCursor.Unset()
}
func (o WebhookDeliveryListResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookDeliveryListResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["items"] = o.Items
if o.NextCursor.IsSet() {
toSerialize["next_cursor"] = o.NextCursor.Get()
}
return toSerialize, nil
}
func (o *WebhookDeliveryListResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"items",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookDeliveryListResponse := _WebhookDeliveryListResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookDeliveryListResponse)
if err != nil {
return err
}
*o = WebhookDeliveryListResponse(varWebhookDeliveryListResponse)
return err
}
type NullableWebhookDeliveryListResponse struct {
value *WebhookDeliveryListResponse
isSet bool
}
func (v NullableWebhookDeliveryListResponse) Get() *WebhookDeliveryListResponse {
return v.value
}
func (v *NullableWebhookDeliveryListResponse) Set(val *WebhookDeliveryListResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookDeliveryListResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookDeliveryListResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookDeliveryListResponse(val *WebhookDeliveryListResponse) *NullableWebhookDeliveryListResponse {
return &NullableWebhookDeliveryListResponse{value: val, isSet: true}
}
func (v NullableWebhookDeliveryListResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookDeliveryListResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,622 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookDeliveryResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookDeliveryResponse{}
// WebhookDeliveryResponse Response model for a webhook delivery record.
type WebhookDeliveryResponse struct {
Id string `json:"id"`
WebhookId NullableString `json:"webhook_id"`
Url string `json:"url"`
EventType string `json:"event_type"`
Status string `json:"status"`
Attempts int32 `json:"attempts"`
NextRetryAt NullableString `json:"next_retry_at,omitempty"`
LastError NullableString `json:"last_error,omitempty"`
LastResponseStatus NullableInt32 `json:"last_response_status,omitempty"`
LastResponseBody NullableString `json:"last_response_body,omitempty"`
LastAttemptAt NullableString `json:"last_attempt_at,omitempty"`
CreatedAt NullableString `json:"created_at,omitempty"`
UpdatedAt NullableString `json:"updated_at,omitempty"`
}
type _WebhookDeliveryResponse WebhookDeliveryResponse
// NewWebhookDeliveryResponse instantiates a new WebhookDeliveryResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookDeliveryResponse(id string, webhookId NullableString, url string, eventType string, status string, attempts int32) *WebhookDeliveryResponse {
this := WebhookDeliveryResponse{}
this.Id = id
this.WebhookId = webhookId
this.Url = url
this.EventType = eventType
this.Status = status
this.Attempts = attempts
return &this
}
// NewWebhookDeliveryResponseWithDefaults instantiates a new WebhookDeliveryResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookDeliveryResponseWithDefaults() *WebhookDeliveryResponse {
this := WebhookDeliveryResponse{}
return &this
}
// GetId returns the Id field value
func (o *WebhookDeliveryResponse) GetId() string {
if o == nil {
var ret string
return ret
}
return o.Id
}
// GetIdOk returns a tuple with the Id field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Id, true
}
// SetId sets field value
func (o *WebhookDeliveryResponse) SetId(v string) {
o.Id = v
}
// GetWebhookId returns the WebhookId field value
// If the value is explicit nil, the zero value for string will be returned
func (o *WebhookDeliveryResponse) GetWebhookId() string {
if o == nil || o.WebhookId.Get() == nil {
var ret string
return ret
}
return *o.WebhookId.Get()
}
// GetWebhookIdOk returns a tuple with the WebhookId field value
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetWebhookIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.WebhookId.Get(), o.WebhookId.IsSet()
}
// SetWebhookId sets field value
func (o *WebhookDeliveryResponse) SetWebhookId(v string) {
o.WebhookId.Set(&v)
}
// GetUrl returns the Url field value
func (o *WebhookDeliveryResponse) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *WebhookDeliveryResponse) SetUrl(v string) {
o.Url = v
}
// GetEventType returns the EventType field value
func (o *WebhookDeliveryResponse) GetEventType() string {
if o == nil {
var ret string
return ret
}
return o.EventType
}
// GetEventTypeOk returns a tuple with the EventType field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetEventTypeOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.EventType, true
}
// SetEventType sets field value
func (o *WebhookDeliveryResponse) SetEventType(v string) {
o.EventType = v
}
// GetStatus returns the Status field value
func (o *WebhookDeliveryResponse) GetStatus() string {
if o == nil {
var ret string
return ret
}
return o.Status
}
// GetStatusOk returns a tuple with the Status field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetStatusOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Status, true
}
// SetStatus sets field value
func (o *WebhookDeliveryResponse) SetStatus(v string) {
o.Status = v
}
// GetAttempts returns the Attempts field value
func (o *WebhookDeliveryResponse) GetAttempts() int32 {
if o == nil {
var ret int32
return ret
}
return o.Attempts
}
// GetAttemptsOk returns a tuple with the Attempts field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetAttemptsOk() (*int32, bool) {
if o == nil {
return nil, false
}
return &o.Attempts, true
}
// SetAttempts sets field value
func (o *WebhookDeliveryResponse) SetAttempts(v int32) {
o.Attempts = v
}
// GetNextRetryAt returns the NextRetryAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetNextRetryAt() string {
if o == nil || IsNil(o.NextRetryAt.Get()) {
var ret string
return ret
}
return *o.NextRetryAt.Get()
}
// GetNextRetryAtOk returns a tuple with the NextRetryAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetNextRetryAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.NextRetryAt.Get(), o.NextRetryAt.IsSet()
}
// HasNextRetryAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasNextRetryAt() bool {
if o != nil && o.NextRetryAt.IsSet() {
return true
}
return false
}
// SetNextRetryAt gets a reference to the given NullableString and assigns it to the NextRetryAt field.
func (o *WebhookDeliveryResponse) SetNextRetryAt(v string) {
o.NextRetryAt.Set(&v)
}
// SetNextRetryAtNil sets the value for NextRetryAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetNextRetryAtNil() {
o.NextRetryAt.Set(nil)
}
// UnsetNextRetryAt ensures that no value is present for NextRetryAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetNextRetryAt() {
o.NextRetryAt.Unset()
}
// GetLastError returns the LastError field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastError() string {
if o == nil || IsNil(o.LastError.Get()) {
var ret string
return ret
}
return *o.LastError.Get()
}
// GetLastErrorOk returns a tuple with the LastError field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastErrorOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastError.Get(), o.LastError.IsSet()
}
// HasLastError returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastError() bool {
if o != nil && o.LastError.IsSet() {
return true
}
return false
}
// SetLastError gets a reference to the given NullableString and assigns it to the LastError field.
func (o *WebhookDeliveryResponse) SetLastError(v string) {
o.LastError.Set(&v)
}
// SetLastErrorNil sets the value for LastError to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastErrorNil() {
o.LastError.Set(nil)
}
// UnsetLastError ensures that no value is present for LastError, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastError() {
o.LastError.Unset()
}
// GetLastResponseStatus returns the LastResponseStatus field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastResponseStatus() int32 {
if o == nil || IsNil(o.LastResponseStatus.Get()) {
var ret int32
return ret
}
return *o.LastResponseStatus.Get()
}
// GetLastResponseStatusOk returns a tuple with the LastResponseStatus field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastResponseStatusOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.LastResponseStatus.Get(), o.LastResponseStatus.IsSet()
}
// HasLastResponseStatus returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastResponseStatus() bool {
if o != nil && o.LastResponseStatus.IsSet() {
return true
}
return false
}
// SetLastResponseStatus gets a reference to the given NullableInt32 and assigns it to the LastResponseStatus field.
func (o *WebhookDeliveryResponse) SetLastResponseStatus(v int32) {
o.LastResponseStatus.Set(&v)
}
// SetLastResponseStatusNil sets the value for LastResponseStatus to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastResponseStatusNil() {
o.LastResponseStatus.Set(nil)
}
// UnsetLastResponseStatus ensures that no value is present for LastResponseStatus, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastResponseStatus() {
o.LastResponseStatus.Unset()
}
// GetLastResponseBody returns the LastResponseBody field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastResponseBody() string {
if o == nil || IsNil(o.LastResponseBody.Get()) {
var ret string
return ret
}
return *o.LastResponseBody.Get()
}
// GetLastResponseBodyOk returns a tuple with the LastResponseBody field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastResponseBodyOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastResponseBody.Get(), o.LastResponseBody.IsSet()
}
// HasLastResponseBody returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastResponseBody() bool {
if o != nil && o.LastResponseBody.IsSet() {
return true
}
return false
}
// SetLastResponseBody gets a reference to the given NullableString and assigns it to the LastResponseBody field.
func (o *WebhookDeliveryResponse) SetLastResponseBody(v string) {
o.LastResponseBody.Set(&v)
}
// SetLastResponseBodyNil sets the value for LastResponseBody to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastResponseBodyNil() {
o.LastResponseBody.Set(nil)
}
// UnsetLastResponseBody ensures that no value is present for LastResponseBody, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastResponseBody() {
o.LastResponseBody.Unset()
}
// GetLastAttemptAt returns the LastAttemptAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastAttemptAt() string {
if o == nil || IsNil(o.LastAttemptAt.Get()) {
var ret string
return ret
}
return *o.LastAttemptAt.Get()
}
// GetLastAttemptAtOk returns a tuple with the LastAttemptAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastAttemptAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastAttemptAt.Get(), o.LastAttemptAt.IsSet()
}
// HasLastAttemptAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastAttemptAt() bool {
if o != nil && o.LastAttemptAt.IsSet() {
return true
}
return false
}
// SetLastAttemptAt gets a reference to the given NullableString and assigns it to the LastAttemptAt field.
func (o *WebhookDeliveryResponse) SetLastAttemptAt(v string) {
o.LastAttemptAt.Set(&v)
}
// SetLastAttemptAtNil sets the value for LastAttemptAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastAttemptAtNil() {
o.LastAttemptAt.Set(nil)
}
// UnsetLastAttemptAt ensures that no value is present for LastAttemptAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastAttemptAt() {
o.LastAttemptAt.Unset()
}
// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetCreatedAt() string {
if o == nil || IsNil(o.CreatedAt.Get()) {
var ret string
return ret
}
return *o.CreatedAt.Get()
}
// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetCreatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.CreatedAt.Get(), o.CreatedAt.IsSet()
}
// HasCreatedAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasCreatedAt() bool {
if o != nil && o.CreatedAt.IsSet() {
return true
}
return false
}
// SetCreatedAt gets a reference to the given NullableString and assigns it to the CreatedAt field.
func (o *WebhookDeliveryResponse) SetCreatedAt(v string) {
o.CreatedAt.Set(&v)
}
// SetCreatedAtNil sets the value for CreatedAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetCreatedAtNil() {
o.CreatedAt.Set(nil)
}
// UnsetCreatedAt ensures that no value is present for CreatedAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetCreatedAt() {
o.CreatedAt.Unset()
}
// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetUpdatedAt() string {
if o == nil || IsNil(o.UpdatedAt.Get()) {
var ret string
return ret
}
return *o.UpdatedAt.Get()
}
// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetUpdatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.UpdatedAt.Get(), o.UpdatedAt.IsSet()
}
// HasUpdatedAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasUpdatedAt() bool {
if o != nil && o.UpdatedAt.IsSet() {
return true
}
return false
}
// SetUpdatedAt gets a reference to the given NullableString and assigns it to the UpdatedAt field.
func (o *WebhookDeliveryResponse) SetUpdatedAt(v string) {
o.UpdatedAt.Set(&v)
}
// SetUpdatedAtNil sets the value for UpdatedAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetUpdatedAtNil() {
o.UpdatedAt.Set(nil)
}
// UnsetUpdatedAt ensures that no value is present for UpdatedAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetUpdatedAt() {
o.UpdatedAt.Unset()
}
func (o WebhookDeliveryResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookDeliveryResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["id"] = o.Id
toSerialize["webhook_id"] = o.WebhookId.Get()
toSerialize["url"] = o.Url
toSerialize["event_type"] = o.EventType
toSerialize["status"] = o.Status
toSerialize["attempts"] = o.Attempts
if o.NextRetryAt.IsSet() {
toSerialize["next_retry_at"] = o.NextRetryAt.Get()
}
if o.LastError.IsSet() {
toSerialize["last_error"] = o.LastError.Get()
}
if o.LastResponseStatus.IsSet() {
toSerialize["last_response_status"] = o.LastResponseStatus.Get()
}
if o.LastResponseBody.IsSet() {
toSerialize["last_response_body"] = o.LastResponseBody.Get()
}
if o.LastAttemptAt.IsSet() {
toSerialize["last_attempt_at"] = o.LastAttemptAt.Get()
}
if o.CreatedAt.IsSet() {
toSerialize["created_at"] = o.CreatedAt.Get()
}
if o.UpdatedAt.IsSet() {
toSerialize["updated_at"] = o.UpdatedAt.Get()
}
return toSerialize, nil
}
func (o *WebhookDeliveryResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"id",
"webhook_id",
"url",
"event_type",
"status",
"attempts",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookDeliveryResponse := _WebhookDeliveryResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookDeliveryResponse)
if err != nil {
return err
}
*o = WebhookDeliveryResponse(varWebhookDeliveryResponse)
return err
}
type NullableWebhookDeliveryResponse struct {
value *WebhookDeliveryResponse
isSet bool
}
func (v NullableWebhookDeliveryResponse) Get() *WebhookDeliveryResponse {
return v.value
}
func (v *NullableWebhookDeliveryResponse) Set(val *WebhookDeliveryResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookDeliveryResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookDeliveryResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookDeliveryResponse(val *WebhookDeliveryResponse) *NullableWebhookDeliveryResponse {
return &NullableWebhookDeliveryResponse{value: val, isSet: true}
}
func (v NullableWebhookDeliveryResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookDeliveryResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,246 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the WebhookHttpConfig type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookHttpConfig{}
// WebhookHttpConfig HTTP delivery configuration for a webhook.
type WebhookHttpConfig struct {
// HTTP method: GET or POST
Method *string `json:"method,omitempty"`
// HTTP request timeout in seconds
TimeoutSeconds *int32 `json:"timeout_seconds,omitempty"`
// Custom HTTP headers
Headers map[string]string `json:"headers,omitempty"`
// Custom HTTP query parameters
Params map[string]string `json:"params,omitempty"`
}
// NewWebhookHttpConfig instantiates a new WebhookHttpConfig object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookHttpConfig() *WebhookHttpConfig {
this := WebhookHttpConfig{}
var method string = "POST"
this.Method = &method
var timeoutSeconds int32 = 30
this.TimeoutSeconds = &timeoutSeconds
return &this
}
// NewWebhookHttpConfigWithDefaults instantiates a new WebhookHttpConfig object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookHttpConfigWithDefaults() *WebhookHttpConfig {
this := WebhookHttpConfig{}
var method string = "POST"
this.Method = &method
var timeoutSeconds int32 = 30
this.TimeoutSeconds = &timeoutSeconds
return &this
}
// GetMethod returns the Method field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetMethod() string {
if o == nil || IsNil(o.Method) {
var ret string
return ret
}
return *o.Method
}
// GetMethodOk returns a tuple with the Method field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetMethodOk() (*string, bool) {
if o == nil || IsNil(o.Method) {
return nil, false
}
return o.Method, true
}
// HasMethod returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasMethod() bool {
if o != nil && !IsNil(o.Method) {
return true
}
return false
}
// SetMethod gets a reference to the given string and assigns it to the Method field.
func (o *WebhookHttpConfig) SetMethod(v string) {
o.Method = &v
}
// GetTimeoutSeconds returns the TimeoutSeconds field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetTimeoutSeconds() int32 {
if o == nil || IsNil(o.TimeoutSeconds) {
var ret int32
return ret
}
return *o.TimeoutSeconds
}
// GetTimeoutSecondsOk returns a tuple with the TimeoutSeconds field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetTimeoutSecondsOk() (*int32, bool) {
if o == nil || IsNil(o.TimeoutSeconds) {
return nil, false
}
return o.TimeoutSeconds, true
}
// HasTimeoutSeconds returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasTimeoutSeconds() bool {
if o != nil && !IsNil(o.TimeoutSeconds) {
return true
}
return false
}
// SetTimeoutSeconds gets a reference to the given int32 and assigns it to the TimeoutSeconds field.
func (o *WebhookHttpConfig) SetTimeoutSeconds(v int32) {
o.TimeoutSeconds = &v
}
// GetHeaders returns the Headers field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetHeaders() map[string]string {
if o == nil || IsNil(o.Headers) {
var ret map[string]string
return ret
}
return o.Headers
}
// GetHeadersOk returns a tuple with the Headers field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetHeadersOk() (map[string]string, bool) {
if o == nil || IsNil(o.Headers) {
return map[string]string{}, false
}
return o.Headers, true
}
// HasHeaders returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasHeaders() bool {
if o != nil && !IsNil(o.Headers) {
return true
}
return false
}
// SetHeaders gets a reference to the given map[string]string and assigns it to the Headers field.
func (o *WebhookHttpConfig) SetHeaders(v map[string]string) {
o.Headers = v
}
// GetParams returns the Params field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetParams() map[string]string {
if o == nil || IsNil(o.Params) {
var ret map[string]string
return ret
}
return o.Params
}
// GetParamsOk returns a tuple with the Params field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetParamsOk() (map[string]string, bool) {
if o == nil || IsNil(o.Params) {
return map[string]string{}, false
}
return o.Params, true
}
// HasParams returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasParams() bool {
if o != nil && !IsNil(o.Params) {
return true
}
return false
}
// SetParams gets a reference to the given map[string]string and assigns it to the Params field.
func (o *WebhookHttpConfig) SetParams(v map[string]string) {
o.Params = v
}
func (o WebhookHttpConfig) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookHttpConfig) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.Method) {
toSerialize["method"] = o.Method
}
if !IsNil(o.TimeoutSeconds) {
toSerialize["timeout_seconds"] = o.TimeoutSeconds
}
if !IsNil(o.Headers) {
toSerialize["headers"] = o.Headers
}
if !IsNil(o.Params) {
toSerialize["params"] = o.Params
}
return toSerialize, nil
}
type NullableWebhookHttpConfig struct {
value *WebhookHttpConfig
isSet bool
}
func (v NullableWebhookHttpConfig) Get() *WebhookHttpConfig {
return v.value
}
func (v *NullableWebhookHttpConfig) Set(val *WebhookHttpConfig) {
v.value = val
v.isSet = true
}
func (v NullableWebhookHttpConfig) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookHttpConfig) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookHttpConfig(val *WebhookHttpConfig) *NullableWebhookHttpConfig {
return &NullableWebhookHttpConfig{value: val, isSet: true}
}
func (v NullableWebhookHttpConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookHttpConfig) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,158 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookListResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookListResponse{}
// WebhookListResponse Response model for listing webhooks.
type WebhookListResponse struct {
Items []WebhookResponse `json:"items"`
}
type _WebhookListResponse WebhookListResponse
// NewWebhookListResponse instantiates a new WebhookListResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookListResponse(items []WebhookResponse) *WebhookListResponse {
this := WebhookListResponse{}
this.Items = items
return &this
}
// NewWebhookListResponseWithDefaults instantiates a new WebhookListResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookListResponseWithDefaults() *WebhookListResponse {
this := WebhookListResponse{}
return &this
}
// GetItems returns the Items field value
func (o *WebhookListResponse) GetItems() []WebhookResponse {
if o == nil {
var ret []WebhookResponse
return ret
}
return o.Items
}
// GetItemsOk returns a tuple with the Items field value
// and a boolean to check if the value has been set.
func (o *WebhookListResponse) GetItemsOk() ([]WebhookResponse, bool) {
if o == nil {
return nil, false
}
return o.Items, true
}
// SetItems sets field value
func (o *WebhookListResponse) SetItems(v []WebhookResponse) {
o.Items = v
}
func (o WebhookListResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookListResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["items"] = o.Items
return toSerialize, nil
}
func (o *WebhookListResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"items",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookListResponse := _WebhookListResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookListResponse)
if err != nil {
return err
}
*o = WebhookListResponse(varWebhookListResponse)
return err
}
type NullableWebhookListResponse struct {
value *WebhookListResponse
isSet bool
}
func (v NullableWebhookListResponse) Get() *WebhookListResponse {
return v.value
}
func (v *NullableWebhookListResponse) Set(val *WebhookListResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookListResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookListResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookListResponse(val *WebhookListResponse) *NullableWebhookListResponse {
return &NullableWebhookListResponse{value: val, isSet: true}
}
func (v NullableWebhookListResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookListResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,446 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookResponse{}
// WebhookResponse Response model for a webhook.
type WebhookResponse struct {
Id string `json:"id"`
BankId NullableString `json:"bank_id"`
Url string `json:"url"`
Secret NullableString `json:"secret,omitempty"`
EventTypes []string `json:"event_types"`
Enabled bool `json:"enabled"`
HttpConfig *WebhookHttpConfig `json:"http_config,omitempty"`
CreatedAt NullableString `json:"created_at,omitempty"`
UpdatedAt NullableString `json:"updated_at,omitempty"`
}
type _WebhookResponse WebhookResponse
// NewWebhookResponse instantiates a new WebhookResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookResponse(id string, bankId NullableString, url string, eventTypes []string, enabled bool) *WebhookResponse {
this := WebhookResponse{}
this.Id = id
this.BankId = bankId
this.Url = url
this.EventTypes = eventTypes
this.Enabled = enabled
return &this
}
// NewWebhookResponseWithDefaults instantiates a new WebhookResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookResponseWithDefaults() *WebhookResponse {
this := WebhookResponse{}
return &this
}
// GetId returns the Id field value
func (o *WebhookResponse) GetId() string {
if o == nil {
var ret string
return ret
}
return o.Id
}
// GetIdOk returns a tuple with the Id field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Id, true
}
// SetId sets field value
func (o *WebhookResponse) SetId(v string) {
o.Id = v
}
// GetBankId returns the BankId field value
// If the value is explicit nil, the zero value for string will be returned
func (o *WebhookResponse) GetBankId() string {
if o == nil || o.BankId.Get() == nil {
var ret string
return ret
}
return *o.BankId.Get()
}
// GetBankIdOk returns a tuple with the BankId field value
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetBankIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.BankId.Get(), o.BankId.IsSet()
}
// SetBankId sets field value
func (o *WebhookResponse) SetBankId(v string) {
o.BankId.Set(&v)
}
// GetUrl returns the Url field value
func (o *WebhookResponse) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *WebhookResponse) SetUrl(v string) {
o.Url = v
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *WebhookResponse) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *WebhookResponse) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *WebhookResponse) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *WebhookResponse) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value
func (o *WebhookResponse) GetEventTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetEventTypesOk() ([]string, bool) {
if o == nil {
return nil, false
}
return o.EventTypes, true
}
// SetEventTypes sets field value
func (o *WebhookResponse) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value
func (o *WebhookResponse) GetEnabled() bool {
if o == nil {
var ret bool
return ret
}
return o.Enabled
}
// GetEnabledOk returns a tuple with the Enabled field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetEnabledOk() (*bool, bool) {
if o == nil {
return nil, false
}
return &o.Enabled, true
}
// SetEnabled sets field value
func (o *WebhookResponse) SetEnabled(v bool) {
o.Enabled = v
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise.
func (o *WebhookResponse) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil || IsNil(o.HttpConfig) {
return nil, false
}
return o.HttpConfig, true
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *WebhookResponse) HasHttpConfig() bool {
if o != nil && !IsNil(o.HttpConfig) {
return true
}
return false
}
// SetHttpConfig gets a reference to the given WebhookHttpConfig and assigns it to the HttpConfig field.
func (o *WebhookResponse) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig = &v
}
// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetCreatedAt() string {
if o == nil || IsNil(o.CreatedAt.Get()) {
var ret string
return ret
}
return *o.CreatedAt.Get()
}
// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetCreatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.CreatedAt.Get(), o.CreatedAt.IsSet()
}
// HasCreatedAt returns a boolean if a field has been set.
func (o *WebhookResponse) HasCreatedAt() bool {
if o != nil && o.CreatedAt.IsSet() {
return true
}
return false
}
// SetCreatedAt gets a reference to the given NullableString and assigns it to the CreatedAt field.
func (o *WebhookResponse) SetCreatedAt(v string) {
o.CreatedAt.Set(&v)
}
// SetCreatedAtNil sets the value for CreatedAt to be an explicit nil
func (o *WebhookResponse) SetCreatedAtNil() {
o.CreatedAt.Set(nil)
}
// UnsetCreatedAt ensures that no value is present for CreatedAt, not even an explicit nil
func (o *WebhookResponse) UnsetCreatedAt() {
o.CreatedAt.Unset()
}
// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetUpdatedAt() string {
if o == nil || IsNil(o.UpdatedAt.Get()) {
var ret string
return ret
}
return *o.UpdatedAt.Get()
}
// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetUpdatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.UpdatedAt.Get(), o.UpdatedAt.IsSet()
}
// HasUpdatedAt returns a boolean if a field has been set.
func (o *WebhookResponse) HasUpdatedAt() bool {
if o != nil && o.UpdatedAt.IsSet() {
return true
}
return false
}
// SetUpdatedAt gets a reference to the given NullableString and assigns it to the UpdatedAt field.
func (o *WebhookResponse) SetUpdatedAt(v string) {
o.UpdatedAt.Set(&v)
}
// SetUpdatedAtNil sets the value for UpdatedAt to be an explicit nil
func (o *WebhookResponse) SetUpdatedAtNil() {
o.UpdatedAt.Set(nil)
}
// UnsetUpdatedAt ensures that no value is present for UpdatedAt, not even an explicit nil
func (o *WebhookResponse) UnsetUpdatedAt() {
o.UpdatedAt.Unset()
}
func (o WebhookResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["id"] = o.Id
toSerialize["bank_id"] = o.BankId.Get()
toSerialize["url"] = o.Url
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
toSerialize["event_types"] = o.EventTypes
toSerialize["enabled"] = o.Enabled
if !IsNil(o.HttpConfig) {
toSerialize["http_config"] = o.HttpConfig
}
if o.CreatedAt.IsSet() {
toSerialize["created_at"] = o.CreatedAt.Get()
}
if o.UpdatedAt.IsSet() {
toSerialize["updated_at"] = o.UpdatedAt.Get()
}
return toSerialize, nil
}
func (o *WebhookResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"id",
"bank_id",
"url",
"event_types",
"enabled",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookResponse := _WebhookResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookResponse)
if err != nil {
return err
}
*o = WebhookResponse(varWebhookResponse)
return err
}
type NullableWebhookResponse struct {
value *WebhookResponse
isSet bool
}
func (v NullableWebhookResponse) Get() *WebhookResponse {
return v.value
}
func (v *NullableWebhookResponse) Set(val *WebhookResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookResponse(val *WebhookResponse) *NullableWebhookResponse {
return &NullableWebhookResponse{value: val, isSet: true}
}
func (v NullableWebhookResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -9,6 +9,7 @@ hindsight_client_api/api/memory_api.py
hindsight_client_api/api/mental_models_api.py
hindsight_client_api/api/monitoring_api.py
hindsight_client_api/api/operations_api.py
hindsight_client_api/api/webhooks_api.py
hindsight_client_api/api_client.py
hindsight_client_api/api_response.py
hindsight_client_api/configuration.py
@@ -35,6 +36,7 @@ hindsight_client_api/models/create_bank_request.py
hindsight_client_api/models/create_directive_request.py
hindsight_client_api/models/create_mental_model_request.py
hindsight_client_api/models/create_mental_model_response.py
hindsight_client_api/models/create_webhook_request.py
hindsight_client_api/models/delete_document_response.py
hindsight_client_api/models/delete_response.py
hindsight_client_api/models/directive_list_response.py
@@ -87,8 +89,14 @@ hindsight_client_api/models/tool_calls_include_options.py
hindsight_client_api/models/update_directive_request.py
hindsight_client_api/models/update_disposition_request.py
hindsight_client_api/models/update_mental_model_request.py
hindsight_client_api/models/update_webhook_request.py
hindsight_client_api/models/validation_error.py
hindsight_client_api/models/validation_error_loc_inner.py
hindsight_client_api/models/version_response.py
hindsight_client_api/models/webhook_delivery_list_response.py
hindsight_client_api/models/webhook_delivery_response.py
hindsight_client_api/models/webhook_http_config.py
hindsight_client_api/models/webhook_list_response.py
hindsight_client_api/models/webhook_response.py
hindsight_client_api/rest.py
hindsight_client_api_README.md
@@ -26,6 +26,7 @@ from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.webhooks_api import WebhooksApi
# import ApiClient
from hindsight_client_api.api_response import ApiResponse
@@ -60,6 +61,7 @@ from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@@ -112,6 +114,12 @@ from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncl
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse
from hindsight_client_api.models.webhook_delivery_list_response import WebhookDeliveryListResponse
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from hindsight_client_api.models.webhook_list_response import WebhookListResponse
from hindsight_client_api.models.webhook_response import WebhookResponse
@@ -10,4 +10,5 @@ from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.webhooks_api import WebhooksApi
File diff suppressed because it is too large Load Diff
@@ -35,6 +35,7 @@ from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@@ -87,6 +88,12 @@ from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncl
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse
from hindsight_client_api.models.webhook_delivery_list_response import WebhookDeliveryListResponse
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from hindsight_client_api.models.webhook_list_response import WebhookListResponse
from hindsight_client_api.models.webhook_response import WebhookResponse
@@ -0,0 +1,104 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class CreateWebhookRequest(BaseModel):
"""
Request model for registering a webhook.
""" # noqa: E501
url: StrictStr = Field(description="HTTP(S) endpoint URL to deliver events to")
secret: Optional[StrictStr] = None
event_types: Optional[List[StrictStr]] = Field(default=None, description="List of event types to deliver. Currently supported: 'consolidation.completed'")
enabled: Optional[StrictBool] = Field(default=True, description="Whether this webhook is active")
http_config: Optional[WebhookHttpConfig] = Field(default=None, description="HTTP delivery configuration (method, timeout, headers, params)")
__properties: ClassVar[List[str]] = ["url", "secret", "event_types", "enabled", "http_config"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of CreateWebhookRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of CreateWebhookRequest from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled") if obj.get("enabled") is not None else True,
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None
})
return _obj
@@ -0,0 +1,124 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class UpdateWebhookRequest(BaseModel):
"""
Request model for updating a webhook. Only provided fields are updated.
""" # noqa: E501
url: Optional[StrictStr] = None
secret: Optional[StrictStr] = None
event_types: Optional[List[StrictStr]] = None
enabled: Optional[StrictBool] = None
http_config: Optional[WebhookHttpConfig] = None
__properties: ClassVar[List[str]] = ["url", "secret", "event_types", "enabled", "http_config"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of UpdateWebhookRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if url (nullable) is None
# and model_fields_set contains the field
if self.url is None and "url" in self.model_fields_set:
_dict['url'] = None
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
# set to None if event_types (nullable) is None
# and model_fields_set contains the field
if self.event_types is None and "event_types" in self.model_fields_set:
_dict['event_types'] = None
# set to None if enabled (nullable) is None
# and model_fields_set contains the field
if self.enabled is None and "enabled" in self.model_fields_set:
_dict['enabled'] = None
# set to None if http_config (nullable) is None
# and model_fields_set contains the field
if self.http_config is None and "http_config" in self.model_fields_set:
_dict['http_config'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of UpdateWebhookRequest from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled"),
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None
})
return _obj
@@ -0,0 +1,102 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from typing import Optional, Set
from typing_extensions import Self
class WebhookDeliveryListResponse(BaseModel):
"""
Response model for listing webhook deliveries.
""" # noqa: E501
items: List[WebhookDeliveryResponse]
next_cursor: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["items", "next_cursor"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookDeliveryListResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of each item in items (list)
_items = []
if self.items:
for _item_items in self.items:
if _item_items:
_items.append(_item_items.to_dict())
_dict['items'] = _items
# set to None if next_cursor (nullable) is None
# and model_fields_set contains the field
if self.next_cursor is None and "next_cursor" in self.model_fields_set:
_dict['next_cursor'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookDeliveryListResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"items": [WebhookDeliveryResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None,
"next_cursor": obj.get("next_cursor")
})
return _obj
@@ -0,0 +1,151 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class WebhookDeliveryResponse(BaseModel):
"""
Response model for a webhook delivery record.
""" # noqa: E501
id: StrictStr
webhook_id: Optional[StrictStr]
url: StrictStr
event_type: StrictStr
status: StrictStr
attempts: StrictInt
next_retry_at: Optional[StrictStr] = None
last_error: Optional[StrictStr] = None
last_response_status: Optional[StrictInt] = None
last_response_body: Optional[StrictStr] = None
last_attempt_at: Optional[StrictStr] = None
created_at: Optional[StrictStr] = None
updated_at: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["id", "webhook_id", "url", "event_type", "status", "attempts", "next_retry_at", "last_error", "last_response_status", "last_response_body", "last_attempt_at", "created_at", "updated_at"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookDeliveryResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# set to None if webhook_id (nullable) is None
# and model_fields_set contains the field
if self.webhook_id is None and "webhook_id" in self.model_fields_set:
_dict['webhook_id'] = None
# set to None if next_retry_at (nullable) is None
# and model_fields_set contains the field
if self.next_retry_at is None and "next_retry_at" in self.model_fields_set:
_dict['next_retry_at'] = None
# set to None if last_error (nullable) is None
# and model_fields_set contains the field
if self.last_error is None and "last_error" in self.model_fields_set:
_dict['last_error'] = None
# set to None if last_response_status (nullable) is None
# and model_fields_set contains the field
if self.last_response_status is None and "last_response_status" in self.model_fields_set:
_dict['last_response_status'] = None
# set to None if last_response_body (nullable) is None
# and model_fields_set contains the field
if self.last_response_body is None and "last_response_body" in self.model_fields_set:
_dict['last_response_body'] = None
# set to None if last_attempt_at (nullable) is None
# and model_fields_set contains the field
if self.last_attempt_at is None and "last_attempt_at" in self.model_fields_set:
_dict['last_attempt_at'] = None
# set to None if created_at (nullable) is None
# and model_fields_set contains the field
if self.created_at is None and "created_at" in self.model_fields_set:
_dict['created_at'] = None
# set to None if updated_at (nullable) is None
# and model_fields_set contains the field
if self.updated_at is None and "updated_at" in self.model_fields_set:
_dict['updated_at'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookDeliveryResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"id": obj.get("id"),
"webhook_id": obj.get("webhook_id"),
"url": obj.get("url"),
"event_type": obj.get("event_type"),
"status": obj.get("status"),
"attempts": obj.get("attempts"),
"next_retry_at": obj.get("next_retry_at"),
"last_error": obj.get("last_error"),
"last_response_status": obj.get("last_response_status"),
"last_response_body": obj.get("last_response_body"),
"last_attempt_at": obj.get("last_attempt_at"),
"created_at": obj.get("created_at"),
"updated_at": obj.get("updated_at")
})
return _obj
@@ -0,0 +1,93 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class WebhookHttpConfig(BaseModel):
"""
HTTP delivery configuration for a webhook.
""" # noqa: E501
method: Optional[StrictStr] = Field(default='POST', description="HTTP method: GET or POST")
timeout_seconds: Optional[StrictInt] = Field(default=30, description="HTTP request timeout in seconds")
headers: Optional[Dict[str, StrictStr]] = Field(default=None, description="Custom HTTP headers")
params: Optional[Dict[str, StrictStr]] = Field(default=None, description="Custom HTTP query parameters")
__properties: ClassVar[List[str]] = ["method", "timeout_seconds", "headers", "params"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookHttpConfig from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookHttpConfig from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"method": obj.get("method") if obj.get("method") is not None else 'POST',
"timeout_seconds": obj.get("timeout_seconds") if obj.get("timeout_seconds") is not None else 30,
"headers": obj.get("headers"),
"params": obj.get("params")
})
return _obj
@@ -0,0 +1,95 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict
from typing import Any, ClassVar, Dict, List
from hindsight_client_api.models.webhook_response import WebhookResponse
from typing import Optional, Set
from typing_extensions import Self
class WebhookListResponse(BaseModel):
"""
Response model for listing webhooks.
""" # noqa: E501
items: List[WebhookResponse]
__properties: ClassVar[List[str]] = ["items"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookListResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of each item in items (list)
_items = []
if self.items:
for _item_items in self.items:
if _item_items:
_items.append(_item_items.to_dict())
_dict['items'] = _items
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookListResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"items": [WebhookResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None
})
return _obj
@@ -0,0 +1,127 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class WebhookResponse(BaseModel):
"""
Response model for a webhook.
""" # noqa: E501
id: StrictStr
bank_id: Optional[StrictStr]
url: StrictStr
secret: Optional[StrictStr] = None
event_types: List[StrictStr]
enabled: StrictBool
http_config: Optional[WebhookHttpConfig] = None
created_at: Optional[StrictStr] = None
updated_at: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["id", "bank_id", "url", "secret", "event_types", "enabled", "http_config", "created_at", "updated_at"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if bank_id (nullable) is None
# and model_fields_set contains the field
if self.bank_id is None and "bank_id" in self.model_fields_set:
_dict['bank_id'] = None
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
# set to None if created_at (nullable) is None
# and model_fields_set contains the field
if self.created_at is None and "created_at" in self.model_fields_set:
_dict['created_at'] = None
# set to None if updated_at (nullable) is None
# and model_fields_set contains the field
if self.updated_at is None and "updated_at" in self.model_fields_set:
_dict['updated_at'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"id": obj.get("id"),
"bank_id": obj.get("bank_id"),
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled"),
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None,
"created_at": obj.get("created_at"),
"updated_at": obj.get("updated_at")
})
return _obj
@@ -32,6 +32,9 @@ import type {
CreateOrUpdateBankData,
CreateOrUpdateBankErrors,
CreateOrUpdateBankResponses,
CreateWebhookData,
CreateWebhookErrors,
CreateWebhookResponses,
DeleteBankData,
DeleteBankErrors,
DeleteBankResponses,
@@ -44,6 +47,9 @@ import type {
DeleteMentalModelData,
DeleteMentalModelErrors,
DeleteMentalModelResponses,
DeleteWebhookData,
DeleteWebhookErrors,
DeleteWebhookResponses,
FileRetainData,
FileRetainErrors,
FileRetainResponses,
@@ -108,6 +114,12 @@ import type {
ListTagsData,
ListTagsErrors,
ListTagsResponses,
ListWebhookDeliveriesData,
ListWebhookDeliveriesErrors,
ListWebhookDeliveriesResponses,
ListWebhooksData,
ListWebhooksErrors,
ListWebhooksResponses,
MetricsEndpointMetricsGetData,
MetricsEndpointMetricsGetResponses,
RecallMemoriesData,
@@ -146,6 +158,9 @@ import type {
UpdateMentalModelData,
UpdateMentalModelErrors,
UpdateMentalModelResponses,
UpdateWebhookData,
UpdateWebhookErrors,
UpdateWebhookResponses,
} from "./types.gen";
export type Options<
@@ -912,6 +927,93 @@ export const triggerConsolidation = <ThrowOnError extends boolean = false>(
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/consolidate", ...options });
/**
* List webhooks
*
* List all webhooks registered for a bank.
*/
export const listWebhooks = <ThrowOnError extends boolean = false>(
options: Options<ListWebhooksData, ThrowOnError>,
) =>
(options.client ?? client).get<
ListWebhooksResponses,
ListWebhooksErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/webhooks", ...options });
/**
* Register webhook
*
* Register a webhook endpoint to receive event notifications for this bank.
*/
export const createWebhook = <ThrowOnError extends boolean = false>(
options: Options<CreateWebhookData, ThrowOnError>,
) =>
(options.client ?? client).post<
CreateWebhookResponses,
CreateWebhookErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
/**
* Delete webhook
*
* Remove a registered webhook.
*/
export const deleteWebhook = <ThrowOnError extends boolean = false>(
options: Options<DeleteWebhookData, ThrowOnError>,
) =>
(options.client ?? client).delete<
DeleteWebhookResponses,
DeleteWebhookErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}", ...options });
/**
* Update webhook
*
* Update one or more fields of a registered webhook. Only provided fields are changed.
*/
export const updateWebhook = <ThrowOnError extends boolean = false>(
options: Options<UpdateWebhookData, ThrowOnError>,
) =>
(options.client ?? client).patch<
UpdateWebhookResponses,
UpdateWebhookErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
/**
* List webhook deliveries
*
* Inspect delivery history for a webhook (useful for debugging).
*/
export const listWebhookDeliveries = <ThrowOnError extends boolean = false>(
options: Options<ListWebhookDeliveriesData, ThrowOnError>,
) =>
(options.client ?? client).get<
ListWebhookDeliveriesResponses,
ListWebhookDeliveriesErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries",
...options,
});
/**
* Clear memory bank memories
*
@@ -618,6 +618,42 @@ export type CreateMentalModelResponse = {
operation_id: string;
};
/**
* CreateWebhookRequest
*
* Request model for registering a webhook.
*/
export type CreateWebhookRequest = {
/**
* Url
*
* HTTP(S) endpoint URL to deliver events to
*/
url: string;
/**
* Secret
*
* HMAC-SHA256 signing secret (optional)
*/
secret?: string | null;
/**
* Event Types
*
* List of event types to deliver. Currently supported: 'consolidation.completed'
*/
event_types?: Array<string>;
/**
* Enabled
*
* Whether this webhook is active
*/
enabled?: boolean;
/**
* HTTP delivery configuration (method, timeout, headers, params)
*/
http_config?: WebhookHttpConfig;
};
/**
* DeleteDocumentResponse
*
@@ -2075,6 +2111,42 @@ export type UpdateMentalModelRequest = {
trigger?: MentalModelTrigger | null;
};
/**
* UpdateWebhookRequest
*
* Request model for updating a webhook. Only provided fields are updated.
*/
export type UpdateWebhookRequest = {
/**
* Url
*
* HTTP(S) endpoint URL
*/
url?: string | null;
/**
* Secret
*
* HMAC-SHA256 signing secret. Omit to keep existing; send null to clear.
*/
secret?: string | null;
/**
* Event Types
*
* List of event types
*/
event_types?: Array<string> | null;
/**
* Enabled
*
* Whether this webhook is active
*/
enabled?: boolean | null;
/**
* HTTP delivery configuration
*/
http_config?: WebhookHttpConfig | null;
};
/**
* ValidationError
*/
@@ -2111,6 +2183,173 @@ export type VersionResponse = {
features: FeaturesInfo;
};
/**
* WebhookDeliveryListResponse
*
* Response model for listing webhook deliveries.
*/
export type WebhookDeliveryListResponse = {
/**
* Items
*/
items: Array<WebhookDeliveryResponse>;
/**
* Next Cursor
*/
next_cursor?: string | null;
};
/**
* WebhookDeliveryResponse
*
* Response model for a webhook delivery record.
*/
export type WebhookDeliveryResponse = {
/**
* Id
*/
id: string;
/**
* Webhook Id
*/
webhook_id: string | null;
/**
* Url
*/
url: string;
/**
* Event Type
*/
event_type: string;
/**
* Status
*/
status: string;
/**
* Attempts
*/
attempts: number;
/**
* Next Retry At
*/
next_retry_at?: string | null;
/**
* Last Error
*/
last_error?: string | null;
/**
* Last Response Status
*/
last_response_status?: number | null;
/**
* Last Response Body
*/
last_response_body?: string | null;
/**
* Last Attempt At
*/
last_attempt_at?: string | null;
/**
* Created At
*/
created_at?: string | null;
/**
* Updated At
*/
updated_at?: string | null;
};
/**
* WebhookHttpConfig
*
* HTTP delivery configuration for a webhook.
*/
export type WebhookHttpConfig = {
/**
* Method
*
* HTTP method: GET or POST
*/
method?: string;
/**
* Timeout Seconds
*
* HTTP request timeout in seconds
*/
timeout_seconds?: number;
/**
* Headers
*
* Custom HTTP headers
*/
headers?: {
[key: string]: string;
};
/**
* Params
*
* Custom HTTP query parameters
*/
params?: {
[key: string]: string;
};
};
/**
* WebhookListResponse
*
* Response model for listing webhooks.
*/
export type WebhookListResponse = {
/**
* Items
*/
items: Array<WebhookResponse>;
};
/**
* WebhookResponse
*
* Response model for a webhook.
*/
export type WebhookResponse = {
/**
* Id
*/
id: string;
/**
* Bank Id
*/
bank_id: string | null;
/**
* Url
*/
url: string;
/**
* Secret
*
* Signing secret (redacted in responses)
*/
secret?: string | null;
/**
* Event Types
*/
event_types: Array<string>;
/**
* Enabled
*/
enabled: boolean;
http_config?: WebhookHttpConfig;
/**
* Created At
*/
created_at?: string | null;
/**
* Updated At
*/
updated_at?: string | null;
};
export type HealthEndpointHealthGetData = {
body?: never;
path?: never;
@@ -3899,6 +4138,217 @@ export type TriggerConsolidationResponses = {
export type TriggerConsolidationResponse =
TriggerConsolidationResponses[keyof TriggerConsolidationResponses];
export type ListWebhooksData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks";
};
export type ListWebhooksErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListWebhooksError = ListWebhooksErrors[keyof ListWebhooksErrors];
export type ListWebhooksResponses = {
/**
* Successful Response
*/
200: WebhookListResponse;
};
export type ListWebhooksResponse =
ListWebhooksResponses[keyof ListWebhooksResponses];
export type CreateWebhookData = {
body: CreateWebhookRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks";
};
export type CreateWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateWebhookError = CreateWebhookErrors[keyof CreateWebhookErrors];
export type CreateWebhookResponses = {
/**
* Successful Response
*/
201: WebhookResponse;
};
export type CreateWebhookResponse =
CreateWebhookResponses[keyof CreateWebhookResponses];
export type DeleteWebhookData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}";
};
export type DeleteWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteWebhookError = DeleteWebhookErrors[keyof DeleteWebhookErrors];
export type DeleteWebhookResponses = {
/**
* Successful Response
*/
200: DeleteResponse;
};
export type DeleteWebhookResponse =
DeleteWebhookResponses[keyof DeleteWebhookResponses];
export type UpdateWebhookData = {
body: UpdateWebhookRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}";
};
export type UpdateWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateWebhookError = UpdateWebhookErrors[keyof UpdateWebhookErrors];
export type UpdateWebhookResponses = {
/**
* Successful Response
*/
200: WebhookResponse;
};
export type UpdateWebhookResponse =
UpdateWebhookResponses[keyof UpdateWebhookResponses];
export type ListWebhookDeliveriesData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: {
/**
* Limit
*
* Maximum number of deliveries to return
*/
limit?: number;
/**
* Cursor
*
* Pagination cursor (created_at of last item)
*/
cursor?: string | null;
};
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries";
};
export type ListWebhookDeliveriesErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListWebhookDeliveriesError =
ListWebhookDeliveriesErrors[keyof ListWebhookDeliveriesErrors];
export type ListWebhookDeliveriesResponses = {
/**
* Successful Response
*/
200: WebhookDeliveryListResponse;
};
export type ListWebhookDeliveriesResponse =
ListWebhookDeliveriesResponses[keyof ListWebhookDeliveriesResponses];
export type ClearBankMemoriesData = {
body?: never;
headers?: {
@@ -0,0 +1,23 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function GET(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const { searchParams } = new URL(request.url);
const limit = searchParams.get("limit") || "50";
const cursor = searchParams.get("cursor");
const qs = new URLSearchParams({ limit });
if (cursor) qs.set("cursor", cursor);
const res = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}/deliveries?${qs}`,
{
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
}
);
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const body = await request.json();
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}`, {
method: "PATCH",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}`, {
method: "DELETE",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
const { bankId } = await params;
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks`, {
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
export async function POST(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
const { bankId } = await params;
const body = await request.json();
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks`, {
method: "POST",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data, { status: 201 });
}
@@ -15,6 +15,7 @@ import { BankConfigView } from "@/components/bank-config-view";
import { BankStatsView } from "@/components/bank-stats-view";
import { BankOperationsView } from "@/components/bank-operations-view";
import { MentalModelsView } from "@/components/mental-models-view";
import { WebhooksView } from "@/components/webhooks-view";
import { useFeatures } from "@/lib/features-context";
import { useBank } from "@/lib/bank-context";
import { client } from "@/lib/api";
@@ -40,7 +41,7 @@ import { Brain, Trash2, Loader2, MoreVertical, Pencil, RotateCcw } from "lucide-
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
type BankConfigTab = "general" | "configuration";
type BankConfigTab = "general" | "configuration" | "webhooks";
export default function BankPage() {
const params = useParams();
@@ -250,6 +251,19 @@ export default function BankPage() {
)}
</button>
)}
<button
onClick={() => handleBankConfigTabChange("webhooks")}
className={`px-6 py-3 font-semibold text-sm transition-all relative ${
bankConfigTab === "webhooks"
? "text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Webhooks
{bankConfigTab === "webhooks" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
</div>
</div>
@@ -272,6 +286,15 @@ export default function BankPage() {
<BankConfigView />
</div>
)}
{bankConfigTab === "webhooks" && (
<div>
<p className="text-sm text-muted-foreground mb-4">
Manage webhook endpoints to receive event notifications from this memory
bank.
</p>
<WebhooksView />
</div>
)}
</div>
</div>
)}
@@ -11,7 +11,6 @@ import {
Users,
ChevronLeft,
ChevronRight,
Box,
Settings,
} from "lucide-react";
import { cn } from "@/lib/utils";
File diff suppressed because it is too large Load Diff
+107
View File
@@ -5,6 +5,40 @@
import { toast } from "sonner";
export interface WebhookHttpConfig {
method: string;
timeout_seconds: number;
headers: Record<string, string>;
params: Record<string, string>;
}
export interface Webhook {
id: string;
bank_id: string | null;
url: string;
event_types: string[];
enabled: boolean;
http_config: WebhookHttpConfig;
created_at: string | null;
updated_at: string | null;
}
export interface WebhookDelivery {
id: string;
webhook_id: string | null;
url: string;
event_type: string;
status: string;
attempts: number;
next_retry_at: string | null;
last_error: string | null;
last_response_status: number | null;
last_response_body: string | null;
last_attempt_at: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface MentalModel {
id: string;
bank_id: string;
@@ -858,6 +892,79 @@ export class ControlPlaneClient {
method: "DELETE",
});
}
/**
* List webhooks for a bank
*/
async listWebhooks(bankId: string): Promise<{ items: Webhook[] }> {
return this.fetchApi<{ items: Webhook[] }>(`/api/banks/${bankId}/webhooks`);
}
/**
* Create a webhook
*/
async createWebhook(
bankId: string,
params: {
url: string;
secret?: string;
event_types?: string[];
enabled?: boolean;
http_config?: WebhookHttpConfig;
}
): Promise<Webhook> {
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks`, {
method: "POST",
body: JSON.stringify(params),
});
}
/**
* Update a webhook (PATCH — only provided fields are changed)
*/
async updateWebhook(
bankId: string,
webhookId: string,
params: {
url?: string;
secret?: string | null;
event_types?: string[];
enabled?: boolean;
http_config?: WebhookHttpConfig;
}
): Promise<Webhook> {
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
method: "PATCH",
body: JSON.stringify(params),
});
}
/**
* Delete a webhook
*/
async deleteWebhook(bankId: string, webhookId: string): Promise<{ success: boolean }> {
return this.fetchApi<{ success: boolean }>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
method: "DELETE",
});
}
/**
* List webhook deliveries
*/
async listWebhookDeliveries(
bankId: string,
webhookId: string,
limit?: number,
cursor?: string
): Promise<{ items: WebhookDelivery[]; next_cursor: string | null }> {
const params = new URLSearchParams();
if (limit) params.append("limit", limit.toString());
if (cursor) params.append("cursor", cursor);
const query = params.toString();
return this.fetchApi<{ items: WebhookDelivery[]; next_cursor: string | null }>(
`/api/banks/${bankId}/webhooks/${webhookId}/deliveries${query ? `?${query}` : ""}`
);
}
}
// Export singleton instance
@@ -654,7 +654,6 @@ async def cmd_generate(bank_id: str, scale: str, workers: int = 16) -> None:
poll_interval_ms=200,
max_slots=workers,
consolidation_max_slots=0,
max_retries=20,
)
poller_task = asyncio.create_task(poller.run())
console.print(" Worker : started\n")
+102 -115
View File
@@ -29,16 +29,9 @@ IGNORE_DIRS = {".git", "notebooks", "node_modules", "__pycache__", ".venv", "ven
def get_docs_dir() -> Path:
"""Find the hindsight-docs directory relative to this script."""
# Navigate from hindsight-dev to hindsight-docs
"""Find the hindsight-docs src/pages/cookbook directory relative to this script."""
script_dir = Path(__file__).parent
docs_dir = script_dir.parent.parent / "hindsight-docs" / "docs" / "cookbook"
return docs_dir
def get_sidebars_file() -> Path:
script_dir = Path(__file__).parent
return script_dir.parent.parent / "hindsight-docs" / "sidebars.ts"
return script_dir.parent.parent / "hindsight-docs" / "src" / "pages" / "cookbook"
def slugify(filename: str) -> str:
@@ -82,30 +75,27 @@ def extract_description_from_notebook(notebook_path: Path) -> str | None:
return None
def extract_tags_from_notebook(notebook_path: Path) -> list[str]:
def extract_tags_from_notebook(notebook_path: Path) -> dict[str, str]:
"""Extract tags from notebook metadata.
Supports both array format and structured object format.
Returns a dict with keys like 'sdk', 'topic', 'language'.
"""
try:
content = json.loads(notebook_path.read_text())
metadata = content.get("metadata", {})
tags = metadata.get("tags", [])
# Array format: ["Python", "Client"]
if isinstance(tags, list):
return tags
# Object format: { "language": "Python", "sdk": "Client", "topic": "Learning" }
# Object format already has the right structure
if isinstance(tags, dict):
result = []
for key in ["language", "sdk", "topic"]:
if key in tags and tags[key]:
result.append(tags[key])
return result
return {k: v for k, v in tags.items() if v}
# Array format: fall back to heuristic conversion
if isinstance(tags, list):
return _infer_tags_from_list(tags)
except Exception:
pass
return []
return {}
def extract_description_from_readme(readme_path: Path) -> str | None:
@@ -129,24 +119,24 @@ def extract_description_from_readme(readme_path: Path) -> str | None:
return None
def extract_tags_from_readme(readme_path: Path) -> list[str]:
def extract_tags_from_readme(readme_path: Path) -> dict[str, str]:
"""Extract tags from frontmatter in README if present.
Supports multiple formats:
- Array: tags: ["Python", "Client"]
- Structured YAML: tags:\n language: "Python"\n sdk: "Client"
- Object literal: tags: { language: "Python", sdk: "Client" }
- Structured YAML: tags:\n sdk: "hindsight-client"\n topic: "Learning"
- Object literal: tags: { sdk: "hindsight-client", topic: "Learning" }
Returns a dict with keys like 'sdk', 'topic', 'language'.
"""
try:
content = readme_path.read_text()
# Check for frontmatter
if content.startswith("---"):
end_idx = content.find("---", 3)
if end_idx > 0:
frontmatter = content[3:end_idx]
lines = frontmatter.split("\n")
# Look for tags: line
for i, line in enumerate(lines):
if line.strip().startswith("tags:"):
tags_str = line.split("tags:", 1)[1].strip()
@@ -154,51 +144,47 @@ def extract_tags_from_readme(readme_path: Path) -> list[str]:
# Inline array format: tags: ["Python", "Client"]
if tags_str.startswith("["):
tags_str = tags_str.strip("[]")
return [t.strip().strip('"').strip("'") for t in tags_str.split(",")]
values = [t.strip().strip('"').strip("'") for t in tags_str.split(",")]
return _infer_tags_from_list(values)
# JavaScript object literal format: tags: { language: "Python", sdk: "Client", topic: "Learning" }
# Object literal: tags: { sdk: "hindsight-client", topic: "Learning" }
if tags_str.startswith("{"):
tags = []
# Extract the entire object literal (might span multiple lines)
obj_str = tags_str
if "}" not in obj_str:
# Multi-line object - collect remaining lines
for j in range(i + 1, len(lines)):
obj_str += " " + lines[j].strip()
if "}" in lines[j]:
break
# Parse the object literal
obj_str = obj_str.strip("{}")
# Split by comma and extract key-value pairs
for pair in obj_str.split(","):
result = {}
for pair in obj_str.strip("{}").split(","):
if ":" in pair:
key, value = pair.split(":", 1)
value = value.strip().strip('"').strip("'")
if value:
tags.append(value)
return tags
k, v = pair.split(":", 1)
k = k.strip().strip('"').strip("'")
v = v.strip().strip('"').strip("'")
if k and v:
result[k] = v
return result
# Structured YAML format:
# Structured YAML:
# tags:
# language: "Python"
# sdk: "Client"
if not tags_str or tags_str == "":
# Parse structured tags from following lines
tags = []
# sdk: "hindsight-client"
# topic: "Learning"
if not tags_str:
result = {}
for j in range(i + 1, len(lines)):
next_line = lines[j].strip()
if not next_line or not next_line.startswith(("language:", "sdk:", "topic:")):
break
# Extract value
if ":" in next_line:
value = next_line.split(":", 1)[1].strip().strip('"').strip("'")
if value:
tags.append(value)
return tags
k, v = next_line.split(":", 1)
k = k.strip()
v = v.strip().strip('"').strip("'")
if k and v:
result[k] = v
return result
except Exception:
pass
return []
return {}
def extract_title_from_readme(readme_path: Path) -> str | None:
@@ -364,6 +350,17 @@ def process_applications(cookbook_dir: Path, apps_dir: Path) -> list[dict]:
if not readme_path.exists():
continue
# Validate that README has frontmatter
readme_raw = readme_path.read_text()
if not readme_raw.startswith("---"):
raise SystemExit(
f"Error: {readme_path} is missing frontmatter.\n"
f"Applications must have a frontmatter block (---) with 'description' and 'tags'."
)
closing = readme_raw.find("---", 3)
if closing <= 0:
raise SystemExit(f"Error: {readme_path} has malformed frontmatter (missing closing ---).")
slug = entry.name
title = extract_title_from_readme(readme_path) or " ".join(word.capitalize() for word in slug.split("-"))
description = extract_description_from_readme(readme_path)
@@ -371,9 +368,10 @@ def process_applications(cookbook_dir: Path, apps_dir: Path) -> list[dict]:
print(f" Processing app: {entry.name}{slug}.md")
# Read README content and strip existing frontmatter
# Read README content, strip existing frontmatter and local .md links
readme_content = readme_path.read_text()
readme_content = strip_frontmatter(readme_content)
readme_content = strip_local_md_links(readme_content)
# Create application page with frontmatter
app_url = f"https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/{entry.name}"
@@ -460,6 +458,14 @@ def update_sidebars(recipes: list[dict], apps: list[dict], sidebars_file: Path):
print("\nUpdated sidebars.ts")
def strip_local_md_links(content: str) -> str:
"""Replace relative .md links with plain text to avoid broken links in Docusaurus.
e.g. [see article](article.md) → see article
"""
return re.sub(r"\[([^\]]+)\]\((?!https?://)([^)]+\.md)\)", r"\1", content)
def clean_description(desc: str) -> str:
"""Clean description for display in carousel cards."""
if not desc:
@@ -482,34 +488,19 @@ def clean_description(desc: str) -> str:
return desc
def convert_tags_to_structured(tags: list[str]) -> dict[str, str]:
"""Convert list of tags to structured format.
def _infer_tags_from_list(tags: list[str]) -> dict[str, str]:
"""Infer sdk/topic structure from a plain list of tag values (legacy array format).
New format has 2 tags:
- sdk: Package name (detected from tag values)
- topic: anything else (Learning, Quick Start, etc.)
Supported languages:
- Node.js: packages starting with '@vectorize-io'
- Go: packages ending with '-go' or containing 'go-'
- Python: everything else
Uses heuristics: package names contain '@' or '-' or start lowercase → sdk,
everything else → topic.
"""
structured = {}
topic_tags = {"Learning", "Quick Start", "Recommendation", "Chat"}
result: dict[str, str] = {}
for tag in tags:
# Check if it's a topic tag
if tag in topic_tags:
structured["topic"] = tag
# Check if it's already a package name (contains @ or -)
elif "@" in tag or (tag and not tag[0].isupper()):
structured["sdk"] = tag
if "@" in tag or (tag and not tag[0].isupper()):
result["sdk"] = tag
else:
# Legacy tag values - map to new format
# For now, treat everything else as SDK/package identifier
structured["sdk"] = tag
return structured
result["topic"] = tag
return result
def update_cookbook_index(recipes: list[dict], apps: list[dict], docs_dir: Path):
@@ -521,21 +512,16 @@ def update_cookbook_index(recipes: list[dict], apps: list[dict], docs_dir: Path)
description = r.get("description", "")
if description:
description = clean_description(description).replace('"', '\\"')
tags = r.get("tags", [])
tags: dict[str, str] = r.get("tags", {})
item = f' {{\n title: "{title}",\n href: "/cookbook/recipes/{r["slug"]}"'
if description:
item += f',\n description: "{description}"'
if tags:
# Convert tags list to structured format
structured_tags = convert_tags_to_structured(tags)
tags_parts = []
if "language" in structured_tags:
tags_parts.append(f'language: "{structured_tags["language"]}"')
if "sdk" in structured_tags:
tags_parts.append(f'sdk: "{structured_tags["sdk"]}"')
if "topic" in structured_tags:
tags_parts.append(f'topic: "{structured_tags["topic"]}"')
for key in ("language", "sdk", "topic"):
if key in tags:
tags_parts.append(f'{key}: "{tags[key]}"')
if tags_parts:
item += f",\n tags: {{ {', '.join(tags_parts)} }}"
item += "\n }"
@@ -550,21 +536,16 @@ def update_cookbook_index(recipes: list[dict], apps: list[dict], docs_dir: Path)
description = a.get("description", "")
if description:
description = clean_description(description).replace('"', '\\"')
tags = a.get("tags", [])
tags = a.get("tags", {})
item = f' {{\n title: "{title}",\n href: "/cookbook/applications/{a["slug"]}"'
if description:
item += f',\n description: "{description}"'
if tags:
# Convert tags list to structured format
structured_tags = convert_tags_to_structured(tags)
tags_parts = []
if "language" in structured_tags:
tags_parts.append(f'language: "{structured_tags["language"]}"')
if "sdk" in structured_tags:
tags_parts.append(f'sdk: "{structured_tags["sdk"]}"')
if "topic" in structured_tags:
tags_parts.append(f'topic: "{structured_tags["topic"]}"')
for key in ("language", "sdk", "topic"):
if key in tags:
tags_parts.append(f'{key}: "{tags[key]}"')
if tags_parts:
item += f",\n tags: {{ {', '.join(tags_parts)} }}"
item += "\n }"
@@ -573,34 +554,42 @@ def update_cookbook_index(recipes: list[dict], apps: list[dict], docs_dir: Path)
apps_json = ",\n".join(app_items)
content = f"""---
sidebar_position: 1
title: Cookbook
hide_table_of_contents: true
pagination_next: null
pagination_prev: null
custom_edit_url: null
sidebar_class_name: hidden-sidebar
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
import CookbookGrid from '@site/src/components/CookbookGrid';
<div className="cookbook-page">
<div>
# Cookbook
<div style={{{{textAlign: 'center', marginBottom: '3.5rem'}}}}>
<h1 style={{{{
fontSize: '3rem',
fontWeight: 800,
background: 'linear-gradient(135deg, #0074d9, #009296)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
letterSpacing: '-0.03em',
lineHeight: 1.15,
marginBottom: '0.75rem',
}}}}>Cookbook</h1>
<p style={{{{fontSize: '1.05rem', color: 'var(--ifm-color-emphasis-600)', maxWidth: 520, margin: '0 auto', lineHeight: 1.7}}}}>
Practical examples and complete applications built with Hindsight.
</p>
</div>
Learn how to build with Hindsight through practical examples:
## Recipes
- **[Recipes](#recipes)** - Step-by-step guides and patterns for common use cases
- **[Applications](#applications)** - Complete, runnable applications demonstrating Hindsight integration
<RecipeCarousel
title="Recipes"
<CookbookGrid
items={{[
{recipes_json}
]}}
/>
<RecipeCarousel
title="Applications"
## Applications
<CookbookGrid
items={{[
{apps_json}
]}}
@@ -682,7 +671,6 @@ def main():
print("Syncing hindsight-cookbook...\n")
docs_dir = get_docs_dir()
sidebars_file = get_sidebars_file()
recipes_dir = docs_dir / "recipes"
apps_dir = docs_dir / "applications"
@@ -758,9 +746,8 @@ def main():
all_recipes = recipes + manual_recipes
all_apps = apps + manual_apps
# Update sidebars.ts and index
# Update cookbook index
if all_recipes or all_apps:
update_sidebars(all_recipes, all_apps, sidebars_file)
update_cookbook_index(all_recipes, all_apps, docs_dir)
print(
@@ -0,0 +1,96 @@
---
sidebar_position: 10
---
# Webhooks
Hindsight can notify your application in real-time when memory events occur by sending HTTP POST requests to a URL you configure.
## Delivery and Retries
Webhooks are registered per memory bank and fire automatically when matching events occur. Each delivery attempt is tracked, and failed deliveries are retried with exponential backoff:
| Attempt | Delay after failure |
|---------|---------------------|
| 1 | 5 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | Permanent failure |
A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within the configured timeout (default 30 seconds). After 6 failed attempts, the delivery is marked as permanently failed and no further retries are made.
:::info At-least-once delivery
Webhook delivery tasks are queued in the same database transaction as the primary operation (e.g. the retain or consolidation write). This means if the server crashes after committing but before sending, the delivery task survives and will be retried. As a result, **your endpoint may receive the same event more than once** — use the `operation_id` field to deduplicate if needed.
:::
## Event Types
### `consolidation.completed`
Fired after Hindsight finishes consolidating new memories into observations for a bank.
**Payload:**
```json
{
"event": "consolidation.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:00Z",
"data": {
"observations_created": 3,
"observations_updated": 1,
"observations_deleted": null,
"error_message": null
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `observations_created` | `integer \| null` | Number of new observations created |
| `observations_updated` | `integer \| null` | Number of existing observations updated |
| `observations_deleted` | `integer \| null` | Number of observations deleted |
| `error_message` | `string \| null` | Set when `status` is `"failed"` |
**`status` values:** `"completed"` or `"failed"`
---
### `retain.completed`
Fired once per document after a retain operation completes (both synchronous and asynchronous). When retaining a batch of N documents, N separate events are fired.
**Payload:**
```json
{
"event": "retain.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:01Z",
"data": {
"document_id": "doc-abc123",
"tags": ["meeting", "q1-2026"]
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `document_id` | `string \| null` | The document ID if one was provided in the retain request |
| `tags` | `string[] \| null` | Document-level tags applied during retain |
**Notes:**
- For async retain (`async: true`), `operation_id` matches the `operation_id` returned by the retain API.
- For sync retain, `operation_id` is a generated identifier for tracing purposes.
- One event is fired per content item in the retain request.
+5
View File
@@ -99,6 +99,11 @@ const sidebars: SidebarsConfig = {
id: 'developer/api/operations',
label: 'Operations',
},
{
type: 'doc',
id: 'developer/api/webhooks',
label: 'Webhooks',
},
],
},
{
@@ -1,3 +1,75 @@
/* Filters */
.filters {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.25rem;
}
.filterGroup {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
}
.filterLabel {
font-size: 0.72rem;
font-weight: 700;
color: var(--ifm-color-emphasis-500);
text-transform: uppercase;
letter-spacing: 0.06em;
min-width: 44px;
}
.filterPill {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.78rem;
font-weight: 500;
padding: 0.2rem 0.65rem;
border-radius: 999px;
border: 1px solid var(--ifm-color-emphasis-300);
background: transparent;
color: var(--ifm-color-emphasis-700);
cursor: pointer;
transition: all 0.15s ease;
font-family: inherit;
}
.filterPill:hover {
border-color: var(--ifm-color-primary);
color: var(--ifm-color-primary);
}
.filterPillActive {
background: var(--ifm-color-primary);
border-color: var(--ifm-color-primary);
color: #fff;
}
.filterPillActive:hover {
color: #fff;
}
.filterPillIcon {
width: 13px;
height: 13px;
object-fit: contain;
flex-shrink: 0;
vertical-align: middle;
}
[data-theme='dark'] .filterPill {
border-color: rgba(255, 255, 255, 0.15);
color: var(--ifm-color-emphasis-600);
}
[data-theme='dark'] .filterPillActive {
color: #fff;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -100,6 +172,9 @@
}
.cardSdk {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.72rem;
font-weight: 500;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
@@ -109,6 +184,13 @@
border-radius: 4px;
}
.sdkIcon {
width: 13px;
height: 13px;
object-fit: contain;
flex-shrink: 0;
}
[data-theme='dark'] .cardSdk {
background: rgba(255, 255, 255, 0.07);
color: var(--ifm-color-emphasis-600);
+89 -6
View File
@@ -1,5 +1,6 @@
import React from 'react';
import React, {useState} from 'react';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './CookbookGrid.module.css';
export interface CookbookCard {
@@ -16,6 +17,26 @@ interface CookbookGridProps {
items: CookbookCard[];
}
function sdkIcon(sdk: string): string | null {
if (sdk.startsWith('@') || sdk.includes('node') || sdk.includes('chat') || sdk.includes('ai-sdk')) {
return '/img/icons/nodejs.png';
}
if (sdk.includes('-go') || sdk === 'go') {
return '/img/icons/golang.png';
}
if (sdk.includes('hindsight-client') || sdk.includes('hindsight-api') || sdk.includes('litellm') || sdk.includes('pydantic') || sdk.includes('crewai')) {
return '/img/icons/python.svg';
}
return null;
}
function SdkIcon({sdk, className}: {sdk: string; className?: string}) {
const icon = sdkIcon(sdk);
const src = useBaseUrl(icon ?? '');
if (!icon) return null;
return <img src={src} alt="" className={className} aria-hidden />;
}
function Card({title, href, description, tags}: CookbookCard) {
return (
<Link to={href} className={styles.card}>
@@ -25,7 +46,12 @@ function Card({title, href, description, tags}: CookbookCard) {
{(tags?.topic || tags?.sdk) && (
<div className={styles.cardFooter}>
{tags.topic && <span className={styles.cardTopic}>{tags.topic}</span>}
{tags.sdk && <span className={styles.cardSdk}>{tags.sdk}</span>}
{tags.sdk && (
<span className={styles.cardSdk}>
<SdkIcon sdk={tags.sdk} className={styles.sdkIcon} />
{tags.sdk}
</span>
)}
</div>
)}
</div>
@@ -34,11 +60,68 @@ function Card({title, href, description, tags}: CookbookCard) {
}
export default function CookbookGrid({items}: CookbookGridProps) {
const [selectedTopic, setSelectedTopic] = useState<string | null>(null);
const [selectedSdk, setSelectedSdk] = useState<string | null>(null);
const topics = [...new Set(items.map((i) => i.tags?.topic).filter(Boolean))] as string[];
const sdks = [...new Set(items.map((i) => i.tags?.sdk).filter(Boolean))] as string[];
const filtered = items.filter((item) => {
if (selectedTopic && item.tags?.topic !== selectedTopic) return false;
if (selectedSdk && item.tags?.sdk !== selectedSdk) return false;
return true;
});
const hasFilters = topics.length > 1 || sdks.length > 1;
return (
<div className={styles.grid}>
{items.map((item) => (
<Card key={item.href} {...item} />
))}
<div>
{hasFilters && (
<div className={styles.filters}>
{topics.length > 1 && (
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>Topic</span>
<button
className={`${styles.filterPill} ${selectedTopic === null ? styles.filterPillActive : ''}`}
onClick={() => setSelectedTopic(null)}>
All
</button>
{topics.map((topic) => (
<button
key={topic}
className={`${styles.filterPill} ${selectedTopic === topic ? styles.filterPillActive : ''}`}
onClick={() => setSelectedTopic(selectedTopic === topic ? null : topic)}>
{topic}
</button>
))}
</div>
)}
{sdks.length > 1 && (
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>SDK</span>
<button
className={`${styles.filterPill} ${selectedSdk === null ? styles.filterPillActive : ''}`}
onClick={() => setSelectedSdk(null)}>
All
</button>
{sdks.map((sdk) => (
<button
key={sdk}
className={`${styles.filterPill} ${selectedSdk === sdk ? styles.filterPillActive : ''}`}
onClick={() => setSelectedSdk(selectedSdk === sdk ? null : sdk)}>
<SdkIcon sdk={sdk} className={styles.filterPillIcon} />
{sdk}
</button>
))}
</div>
)}
</div>
)}
<div className={styles.grid}>
{filtered.map((item) => (
<Card key={item.href} {...item} />
))}
</div>
</div>
);
}
+1 -1
View File
@@ -396,7 +396,7 @@ a.menu__link[href*="/sdks/python"]::before {
/* Node.js logo */
a.menu__link[href*="/sdks/nodejs"]::before {
background-image: url('/img/icons/nodejs.svg');
background-image: url('/img/icons/nodejs.png');
}
/* CLI - terminal icon */
@@ -0,0 +1,143 @@
---
sidebar_position: 1
---
# CableConnect — AI Customer Service Copilot Demo
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/cable-co)
:::
An AI copilot that assists a customer service representative (CSR) by suggesting responses and actions for simulated customer scenarios. The CSR approves or rejects each suggestion with feedback. The copilot learns from corrections via [Hindsight](https://hindsight.vectorize.io) and stops repeating mistakes.
## Prerequisites
- Python 3.11+
- Node.js 18+
- An OpenAI API key (for GPT-4o)
- A Hindsight API key ([sign up](https://hindsight.vectorize.io))
## Quick Start
### 1. Backend
```bash
cd backend
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Create a .env file with your credentials
cat > .env << 'EOF'
OPENAI_API_KEY=sk-your-openai-key
HINDSIGHT_API_KEY=hsk_your-hindsight-key
HINDSIGHT_API_URL=https://api.hindsight.vectorize.io
HINDSIGHT_BANK_NAME=cable-connect-demo
EOF
# Start the backend (port 8002)
./run.sh
```
### 2. Frontend
In a second terminal:
```bash
cd frontend
# Install dependencies
npm install
# Start the dev server (port 5173)
npm run dev
```
Open http://localhost:5173 in your browser.
## Running the Demo
1. Click **Next Customer** to load the first scenario
2. The AI copilot will analyze the customer's issue and suggest a response
3. Review the suggestion in the right panel:
- **Send to Customer** — approves the response, sends it to the customer chat
- **Approve** — executes a system action (credit, dispatch, etc.)
- **Reject** — type feedback explaining what was wrong
4. The copilot adjusts based on your feedback and tries again
5. Continue until the customer is satisfied, then approve the resolve action
6. Click **Next Customer** for the next scenario
### What to Watch For
The 8 scenarios include 3 **learning pairs** — the first scenario teaches the agent a rule, the second tests whether it remembers:
| Pair | Scenarios | What the Agent Learns |
|------|-----------|----------------------|
| A | 2 then 4 | Credit adjustments are capped at $25 |
| B | 3 then 8 | Run remote diagnostics before scheduling a dispatch |
| C | 5 then 6 | Retention offers require 24+ months of tenure |
With **Memory On** (the default), the copilot recalls past CSR feedback before each new customer. By the test scenario, it should handle the situation correctly without being corrected.
Toggle **Memory Off** to see how the agent behaves without learning — it will make the same mistakes every time.
### Controls
- **Mode** dropdown — Switch between Memory On and Memory Off
- **Reset** — Deletes all stored memories and starts the scenario queue over
- **Refresh Models** — Manually triggers a refresh of the agent's mental models
## Configuration
All configuration is via environment variables in `backend/.env`:
| Variable | Default | Description |
|----------|---------|-------------|
| `OPENAI_API_KEY` | — | Your OpenAI API key (required) |
| `HINDSIGHT_API_KEY` | — | Your Hindsight API key (required) |
| `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API endpoint |
| `HINDSIGHT_BANK_NAME` | `cable-connect-demo` | Name of the memory bank |
| `LLM_MODEL` | `openai/gpt-4o` | LLM model (via LiteLLM format) |
| `BACKEND_PORT` | `8002` | Backend server port |
## Project Structure
```
cable-co/
├── backend/
│ ├── run.sh # Start script (loads .env, runs uvicorn)
│ ├── requirements.txt
│ ├── telecom_data.py # Accounts, plans, billing, outages, scenarios
│ ├── agent_tools.py # 19 tools + business rule hints
│ └── app/
│ ├── main.py # FastAPI + WebSocket
│ ├── config.py
│ └── services/
│ ├── agent_service.py # Copilot loop with CSR approval gate
│ └── memory_service.py # Hindsight retain/recall/mental models
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── App.tsx
│ ├── stores/sessionStore.ts
│ ├── hooks/useWebSocket.ts
│ └── components/
│ ├── ControlBar.tsx
│ ├── CustomerChat.tsx
│ ├── CopilotChat.tsx
│ ├── KnowledgePanel.tsx
│ └── MentalModelsPanel.tsx
└── article.md # Detailed writeup of how agent learning works
```
## How It Works
See article.md for a detailed explanation of the agent learning architecture, including how Hindsight transforms CSR feedback into observations and mental models that improve the copilot's behavior over time.
@@ -0,0 +1,122 @@
---
sidebar_position: 3
---
# Chat Memory App (Hindsight Cloud)
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/chat-memory-cloud)
:::
A demo chat application with persistent per-user memory powered by [Hindsight Cloud](https://hindsight.vectorize.io). Supports OpenAI or Groq as the LLM provider. No local Hindsight server required.
## Features
- 🧠 **Persistent Memory**: Each user gets their own memory bank that remembers conversations
- ☁️ **Hindsight Cloud**: Memory stored in the cloud — no Docker setup needed
- 🔀 **Selectable LLM**: Choose between OpenAI (GPT-4o) or Groq (Qwen 32B)
- 🎯 **Per-User Context**: Isolated memory per user with automatic context retrieval
- 💬 **Real-time Chat**: Instant responses with memory-augmented context
## Setup
### 1. Get API Keys
- **Hindsight** — Sign up at https://hindsight.vectorize.io
- **OpenAI** — https://platform.openai.com/api-keys
- **Groq** (alternative) — Free at https://console.groq.com/home
### 2. Configure Environment
Edit `.env.local` with your API keys and preferred provider:
```bash
# LLM Provider: "openai" or "groq"
LLM_PROVIDER=openai
# OpenAI (required if LLM_PROVIDER=openai)
OPENAI_API_KEY=sk-your-key-here
# Groq (required if LLM_PROVIDER=groq)
GROQ_API_KEY=gsk_your-key-here
# Hindsight Cloud
HINDSIGHT_API_URL=https://api.hindsight.vectorize.io
HINDSIGHT_API_KEY=hsk_your-key-here
```
You can also override the model with `LLM_MODEL` (defaults to `gpt-4o` for OpenAI, `qwen/qwen3-32b` for Groq).
### 3. Install Dependencies
```bash
npm install
```
### 4. Run the App
```bash
npm run dev
```
Open http://localhost:3000 in your browser.
## How It Works
1. **User Identity**: Each browser session gets a unique user ID
2. **Memory Bank Creation**: First message creates a personal memory bank in Hindsight Cloud
3. **Context Retrieval**: Before responding, relevant memories are recalled
4. **Memory Augmented Response**: LLM generates responses with memory context
5. **Conversation Storage**: Each conversation is retained for future context
## Architecture
```
User Message
Next.js API Route (/api/chat)
Hindsight Cloud recall() → Get relevant memories
OpenAI or Groq → Generate response with memory context
Hindsight Cloud retain() → Store conversation
Response to User
```
## Memory Bank Structure
Each user gets their own isolated memory bank with:
- **Name**: "Chat Memory for [userId]"
- **Background**: Conversational AI assistant context
- **Disposition**: Empathetic (4), Low Skepticism (2), Balanced Literalism (3)
## Try It Out
1. **First Conversation**: Tell the assistant about yourself
- "Hi! I'm a software engineer from San Francisco. I love Python and machine learning."
2. **Second Conversation**: Ask what it remembers
- "What do you know about me?"
- "What programming languages do I like?"
3. **Context Building**: Continue sharing preferences
- "I prefer VS Code over other editors"
- "I'm working on a React project"
4. **Memory Verification**: Log in to the [Hindsight dashboard](https://hindsight.vectorize.io) to see stored memories
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `LLM_PROVIDER` | `openai` | LLM provider: `openai` or `groq` |
| `LLM_MODEL` | auto | Model override (defaults: `gpt-4o` / `qwen/qwen3-32b`) |
| `OPENAI_API_KEY` | — | Required when using OpenAI |
| `GROQ_API_KEY` | — | Required when using Groq |
| `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API endpoint |
| `HINDSIGHT_API_KEY` | — | Your Hindsight API key |
@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 2
---
# Chat Memory App
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 4
---
# Chat SDK Multi-Platform Bot
@@ -0,0 +1,81 @@
---
sidebar_position: 5
---
# ClaimsIQ — Insurance Claims Triage Agent Demo
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/claims-iq)
:::
An AI agent that processes insurance claims through a multi-step workflow. The agent starts as a "confused rookie" and becomes a "seasoned expert" as [Hindsight](https://github.com/anthropics/hindsight) memories accumulate.
Watch the agent learn coverage rules, adjuster assignments, and escalation patterns in real-time through a pipeline dashboard.
## Quick Start
### 1. Start Hindsight API (port 8888)
```bash
docker run -p 8888:8888 ghcr.io/anthropics/hindsight:latest
```
### 2. Start Backend (port 8000)
```bash
cd backend
pip install -r requirements.txt
./run.sh
```
### 3. Start Frontend (port 5173)
```bash
cd frontend
npm install
npm run dev
```
### 4. Open Browser
Navigate to `http://localhost:5173`.
## How It Works
The agent processes insurance claims using 6 tools:
1. **Classify** the claim category (auto, property, flood, etc.)
2. **Look up** the policy details
3. **Check coverage** rules for the policy type
4. **Check fraud** indicators
5. **Assign** the right adjuster
6. **Submit** a decision for validation
The system validates each decision against ground truth. If the agent makes a mistake (wrong adjuster, incorrect coverage call), the decision is rejected with feedback — creating learning signal for Hindsight.
## Agent Modes
| Mode | Description |
|------|-------------|
| **No Memory** | Baseline — agent starts fresh every claim |
| **Recall** | Raw facts from past claims injected before processing |
| **Reflect** | LLM-synthesized knowledge injected |
| **Mental Models** | Full Hindsight mental models with auto-refresh |
## Key Learning Challenges
- **Water damage vs Flood**: Gold policies cover water damage (burst pipe) but NOT flood damage (rain/river). The agent must learn this subtle distinction.
- **Adjuster routing**: 8 adjusters with different specialties and regions. The agent must learn who handles what.
- **Escalation thresholds**: Claims over $50K need a senior adjuster; over $100K need manager review.
- **Fraud detection**: Multiple indicators (near-limit claims, repeated address) route to the fraud specialist.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LLM_MODEL` | `openai/gpt-4o` | LLM model for the agent |
| `HINDSIGHT_API_URL` | `http://localhost:8888` | Hindsight API URL |
| `BACKEND_PORT` | `8000` | Backend server port |
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 6
---
# CrewAI + Hindsight Memory
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 7
---
# Deliveryman Demo
@@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 8
---
# Go Memory-Augmented API
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 9
---
# Memory Approaches Comparison Demo
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 10
---
# Tool Learning Demo
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 11
---
# OpenAI Agent + Hindsight Memory Integration
@@ -0,0 +1,236 @@
---
sidebar_position: 12
---
# Pydantic AI + Hindsight Memory
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/pydantic-ai-memory)
:::
Give your Pydantic AI agents persistent long-term memory. Chat with an assistant multiple times and watch it remember what you told it in previous sessions.
## What This Demonstrates
- **Memory tools** — retain, recall, and reflect via `create_hindsight_tools()`
- **Auto-injected context** — relevant memories in every run via `memory_instructions()`
- **Persistent memory across sessions** — the agent remembers between script runs
- **Interactive chat loop** with message history reuse
## Architecture
```
Session 1:
You: "I'm a Python developer working on a FastAPI project"
├─ memory_instructions() ──► recalls prior context (empty on first run)
├─ Agent decides to call hindsight_retain ──► stores the fact
└─ Agent responds with acknowledgement
Session 2:
You: "What do you know about me?"
├─ memory_instructions() ──► injects "User is a Python developer..."
├─ Agent calls hindsight_recall ──► finds stored facts
└─ Agent responds with everything it remembers
```
## Prerequisites
1. **Hindsight running**
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
2. **OpenAI API key** (for Pydantic AI's LLM)
```bash
export OPENAI_API_KEY=your-key
```
3. **Install dependencies**
```bash
cd applications/pydantic-ai-memory
pip install -r requirements.txt
```
## Quick Start
### Interactive Chat
```bash
python personal_assistant.py
```
Example session:
```
Personal assistant ready (bank: personal-assistant)
Type 'quit' or 'exit' to stop.
You: I'm a Python developer and I love hiking on weekends
Assistant: I've noted that! You're a Python developer who enjoys weekend hiking.
You: What do you know about me?
Assistant: From my memory, I know that you're a Python developer and you
love hiking on weekends.
You: quit
```
Run it again — the agent still remembers:
```
You: What are my hobbies?
Assistant: Based on my memories, you enjoy hiking on weekends!
```
### Single Query
```bash
python personal_assistant.py "What do you remember about my preferences?"
```
### Reset Memory
```bash
python personal_assistant.py --reset
```
## How It Works
### 1. Create a Hindsight Client
```python
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
```
### 2. Create Memory Tools
`create_hindsight_tools()` returns Pydantic AI `Tool` instances the agent can call:
```python
from hindsight_pydantic_ai import create_hindsight_tools
tools = create_hindsight_tools(client=client, bank_id="personal-assistant")
# Returns: [hindsight_retain, hindsight_recall, hindsight_reflect]
```
### 3. Add Memory Instructions
`memory_instructions()` returns an async callable that auto-recalls relevant memories and injects them into the system prompt on every run:
```python
from hindsight_pydantic_ai import memory_instructions
instructions_fn = memory_instructions(
client=client,
bank_id="personal-assistant",
query="important context about the user",
max_results=5,
)
```
### 4. Wire Up the Agent
```python
from pydantic_ai import Agent
agent = Agent(
"openai:gpt-4o-mini",
system_prompt="You are a helpful assistant with long-term memory...",
tools=tools,
instructions=[instructions_fn],
)
result = await agent.run("What do you know about me?")
```
## Core Files
| File | Description |
|------|-------------|
| `personal_assistant.py` | Complete working example with interactive chat and single-query modes |
| `requirements.txt` | Python dependencies |
## Customization
### Use Only Tools (No Auto-Injection)
Let the agent decide when to search memory, rather than always injecting context:
```python
agent = Agent(
"openai:gpt-4o-mini",
tools=create_hindsight_tools(client=client, bank_id="my-bank"),
)
```
### Use Only Instructions (No Tools)
Auto-inject memories without giving the agent explicit retain/recall/reflect tools:
```python
agent = Agent(
"openai:gpt-4o-mini",
instructions=[memory_instructions(client=client, bank_id="my-bank")],
)
```
### Select Specific Tools
```python
tools = create_hindsight_tools(
client=client,
bank_id="my-bank",
include_retain=True,
include_recall=True,
include_reflect=False, # Omit reflect
)
```
### Use a Different Model
Any [Pydantic AI model](https://ai.pydantic.dev/models/) works:
```python
agent = Agent(
"anthropic:claude-sonnet-4-20250514",
tools=create_hindsight_tools(client=client, bank_id="my-bank"),
)
```
## Common Issues
**"Connection refused"**
- Make sure Hindsight is running on `localhost:8888`
**"OPENAI_API_KEY not set"**
```bash
export OPENAI_API_KEY=your-key
```
**"No module named 'hindsight_pydantic_ai'"**
```bash
pip install -r requirements.txt
```
---
**Built with:**
- [Pydantic AI](https://ai.pydantic.dev) - Type-safe AI agent framework
- [hindsight-pydantic-ai](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/pydantic-ai) - Hindsight memory tools for Pydantic AI
- [Hindsight](https://github.com/vectorize-io/hindsight) - Long-term memory for AI agents
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 13
---
# Sanity CMS Blog Memory
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 14
---
# Stance Tracker
@@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 15
---
# Hindsight AI SDK - Personal Chef
+31 -7
View File
@@ -101,11 +101,23 @@ import CookbookGrid from '@site/src/components/CookbookGrid';
<CookbookGrid
items={[
{
title: "CableConnect — AI Customer Service Copilot Demo",
href: "/cookbook/applications/cable-co",
description: "AI customer service copilot that learns from CSR feedback via Hindsight",
tags: { sdk: "hindsight-client", topic: "Customer Service" }
},
{
title: "Chat Memory App",
href: "/cookbook/applications/chat-memory",
description: "Real-time chat app with per-user memory using Groq and Hindsight",
tags: { sdk: "hindsight-client", topic: "Chat" }
tags: { sdk: "@vectorize-io/hindsight-client", topic: "Chat" }
},
{
title: "Chat Memory App (Hindsight Cloud)",
href: "/cookbook/applications/chat-memory-cloud",
description: "Real-time chat app with per-user memory powered by Hindsight Cloud",
tags: { sdk: "@vectorize-io/hindsight-client", topic: "Chat" }
},
{
title: "Chat SDK Multi-Platform Bot",
@@ -113,23 +125,29 @@ import CookbookGrid from '@site/src/components/CookbookGrid';
description: "Multi-platform chat bot with cross-platform memory using Vercel Chat SDK and Hindsight",
tags: { sdk: "@vectorize-io/hindsight-chat", topic: "Recommendation" }
},
{
title: "ClaimsIQ — Insurance Claims Triage Agent Demo",
href: "/cookbook/applications/claims-iq",
description: "Insurance claims triage agent that learns adjudication rules via Hindsight",
tags: { sdk: "hindsight-litellm", topic: "Agents" }
},
{
title: "CrewAI + Hindsight Memory",
href: "/cookbook/applications/crewai-memory",
description: "CrewAI agents with persistent long-term memory via Hindsight",
tags: { sdk: "Agents" }
tags: { sdk: "hindsight-crewai", topic: "Agents" }
},
{
title: "Deliveryman Demo",
href: "/cookbook/applications/deliveryman-demo",
description: "Delivery agent simulation demonstrating learning through mental models",
tags: { sdk: "hindsight-client", topic: "Learning" }
tags: { sdk: "hindsight-litellm", topic: "Learning" }
},
{
title: "Go Memory-Augmented API",
href: "/cookbook/applications/go-memory-service",
description: "Go HTTP microservice with per-user memory banks for a developer knowledge assistant",
tags: { sdk: "hindsight-go", topic: "Learning" }
tags: { sdk: "hindsight-client-go", topic: "Learning" }
},
{
title: "Memory Approaches Comparison Demo",
@@ -147,19 +165,25 @@ import CookbookGrid from '@site/src/components/CookbookGrid';
title: "OpenAI Agent + Hindsight Memory Integration",
href: "/cookbook/applications/openai-fitness-coach",
description: "Fitness coach using OpenAI Assistants with Hindsight as memory backend",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
tags: { sdk: "hindsight-api", topic: "Recommendation" }
},
{
title: "Pydantic AI + Hindsight Memory",
href: "/cookbook/applications/pydantic-ai-memory",
description: "Pydantic AI agent with persistent long-term memory via Hindsight",
tags: { sdk: "hindsight-pydantic-ai", topic: "Agents" }
},
{
title: "Sanity CMS Blog Memory",
href: "/cookbook/applications/sanity-blog-memory",
description: "Sync Sanity CMS blog posts to Hindsight for semantic search and AI insights",
tags: { sdk: "hindsight-client", topic: "Learning" }
tags: { sdk: "@vectorize-io/hindsight-client", topic: "Learning" }
},
{
title: "Stance Tracker",
href: "/cookbook/applications/stancetracker",
description: "Track political candidates' stances over time with automated web scraping",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
tags: { sdk: "@vectorize-io/hindsight-client", topic: "Recommendation" }
},
{
title: "Hindsight AI SDK - Personal Chef",
Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.8 KiB

+770
View File
@@ -3141,6 +3141,375 @@
}
}
},
"/v1/default/banks/{bank_id}/webhooks": {
"post": {
"tags": [
"Webhooks"
],
"summary": "Register webhook",
"description": "Register a webhook endpoint to receive event notifications for this bank.",
"operationId": "create_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWebhookRequest"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"tags": [
"Webhooks"
],
"summary": "List webhooks",
"description": "List all webhooks registered for a bank.",
"operationId": "list_webhooks",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookListResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}": {
"delete": {
"tags": [
"Webhooks"
],
"summary": "Delete webhook",
"description": "Remove a registered webhook.",
"operationId": "delete_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"patch": {
"tags": [
"Webhooks"
],
"summary": "Update webhook",
"description": "Update one or more fields of a registered webhook. Only provided fields are changed.",
"operationId": "update_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWebhookRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries": {
"get": {
"tags": [
"Webhooks"
],
"summary": "List webhook deliveries",
"description": "Inspect delivery history for a webhook (useful for debugging).",
"operationId": "list_webhook_deliveries",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 200,
"description": "Maximum number of deliveries to return",
"default": 50,
"title": "Limit"
},
"description": "Maximum number of deliveries to return"
},
{
"name": "cursor",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Pagination cursor (created_at of last item)",
"title": "Cursor"
},
"description": "Pagination cursor (created_at of last item)"
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookDeliveryListResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/memories": {
"post": {
"tags": [
@@ -4348,6 +4717,54 @@
"title": "CreateMentalModelResponse",
"description": "Response model for mental model creation."
},
"CreateWebhookRequest": {
"properties": {
"url": {
"type": "string",
"title": "Url",
"description": "HTTP(S) endpoint URL to deliver events to"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "HMAC-SHA256 signing secret (optional)"
},
"event_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Event Types",
"description": "List of event types to deliver. Currently supported: 'consolidation.completed'",
"default": [
"consolidation.completed"
]
},
"enabled": {
"type": "boolean",
"title": "Enabled",
"description": "Whether this webhook is active",
"default": true
},
"http_config": {
"$ref": "#/components/schemas/WebhookHttpConfig",
"description": "HTTP delivery configuration (method, timeout, headers, params)"
}
},
"type": "object",
"required": [
"url"
],
"title": "CreateWebhookRequest",
"description": "Request model for registering a webhook."
},
"DeleteDocumentResponse": {
"properties": {
"success": {
@@ -7031,6 +7448,75 @@
}
}
},
"UpdateWebhookRequest": {
"properties": {
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Url",
"description": "HTTP(S) endpoint URL"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "HMAC-SHA256 signing secret. Omit to keep existing; send null to clear."
},
"event_types": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Event Types",
"description": "List of event types"
},
"enabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Enabled",
"description": "Whether this webhook is active"
},
"http_config": {
"anyOf": [
{
"$ref": "#/components/schemas/WebhookHttpConfig"
},
{
"type": "null"
}
],
"description": "HTTP delivery configuration"
}
},
"type": "object",
"title": "UpdateWebhookRequest",
"description": "Request model for updating a webhook. Only provided fields are updated."
},
"ValidationError": {
"properties": {
"loc": {
@@ -7093,6 +7579,290 @@
"worker": true
}
}
},
"WebhookDeliveryListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/WebhookDeliveryResponse"
},
"type": "array",
"title": "Items"
},
"next_cursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Next Cursor"
}
},
"type": "object",
"required": [
"items"
],
"title": "WebhookDeliveryListResponse",
"description": "Response model for listing webhook deliveries."
},
"WebhookDeliveryResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"webhook_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Webhook Id"
},
"url": {
"type": "string",
"title": "Url"
},
"event_type": {
"type": "string",
"title": "Event Type"
},
"status": {
"type": "string",
"title": "Status"
},
"attempts": {
"type": "integer",
"title": "Attempts"
},
"next_retry_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Next Retry At"
},
"last_error": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Error"
},
"last_response_status": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Last Response Status"
},
"last_response_body": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Response Body"
},
"last_attempt_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Attempt At"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"updated_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Updated At"
}
},
"type": "object",
"required": [
"id",
"webhook_id",
"url",
"event_type",
"status",
"attempts"
],
"title": "WebhookDeliveryResponse",
"description": "Response model for a webhook delivery record."
},
"WebhookHttpConfig": {
"properties": {
"method": {
"type": "string",
"title": "Method",
"description": "HTTP method: GET or POST",
"default": "POST"
},
"timeout_seconds": {
"type": "integer",
"title": "Timeout Seconds",
"description": "HTTP request timeout in seconds",
"default": 30
},
"headers": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"title": "Headers",
"description": "Custom HTTP headers"
},
"params": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"title": "Params",
"description": "Custom HTTP query parameters"
}
},
"type": "object",
"title": "WebhookHttpConfig",
"description": "HTTP delivery configuration for a webhook."
},
"WebhookListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/WebhookResponse"
},
"type": "array",
"title": "Items"
}
},
"type": "object",
"required": [
"items"
],
"title": "WebhookListResponse",
"description": "Response model for listing webhooks."
},
"WebhookResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"bank_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Bank Id"
},
"url": {
"type": "string",
"title": "Url"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "Signing secret (redacted in responses)"
},
"event_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Event Types"
},
"enabled": {
"type": "boolean",
"title": "Enabled"
},
"http_config": {
"$ref": "#/components/schemas/WebhookHttpConfig"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"updated_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Updated At"
}
},
"type": "object",
"required": [
"id",
"bank_id",
"url",
"event_types",
"enabled"
],
"title": "WebhookResponse",
"description": "Response model for a webhook."
}
}
}
+1 -1
View File
@@ -14,4 +14,4 @@ echo ""
echo "Starting Docusaurus development server..."
echo "Documentation will be available at: http://localhost:3000"
echo ""
npm run start -w hindsight-docs
npm run start -w hindsight-docs -- --no-open