目次
はじめに

クイックスタート

解約リスクの例をmockのConnectorで動かし、検査、計画、Checkpointでの停止、承認、Resultの読み取りまでを一通り試す。

10分で、1つのプログラムを動かします。Salesforceから取引先を、Snowflakeから利用データを読み、どの取引先に解約リスクがあるかをLLMに判定させ、承認のために止まり、答えをSalesforceに書き戻すプログラムです。相手はすべてmockのシステムなので、本物には何も触れず、認証情報も要りません。まずCLIから、次にTypeScriptとPythonから動かします。

先にCLIをインストールしてください(インストール)。

npm install -g actql

1. プロジェクト

プロジェクトはCLIに作らせます。

actql init --template churn-risk-mock churn-risk
cd churn-risk

init は7つのファイルと、Hostの状態を置く .actql ディレクトリを書き出します。Runが使うものはすべてこのファイルの中にあるので、どのマシンでも同じ結果になります。手で作ってもかまいません。順に見ていきます。

actql.config.json は、状態と定義の置き場所をCLIに伝えます。mockプロファイルは、すべてのConnectorとLLMを、mock に指定したフィクスチャのファイルから答える代役に置き換えます。ポリシーはプロジェクトの中にはありません。Hostの設定(~/.config/actql/host.json)がまだなければ init が作ります。account_config がなければ、ローカルのHostの標準のポリシーで、設定した接続とタスクに対する読み取りと INFER はそのまま動き、すべてのEffectは effect_policy に委ねられます。その既定の review が、承認まで止めます。

{
  "version": "actql-config/0.1",
  "connectors": "./connectors.json",
  "model_tasks": "./model-tasks.json",
  "state_dir": "./.actql",
  "profile": "mock",
  "mock": {
    "data": "./mock/data.json",
    "answers": {"model-task:churn-risk.v1": "./mock/churn-risk.v1.answers.yaml"}
  }
}

connectors.json は、2つの接続 salesforce と snowflake と、それぞれが使うConnectorを指定します。mockプロファイルでは認証情報は読まれないので、まだ何も持っていません。実際のシステムに向けるときに何を足すかは各Connectorのページにあります。

model-tasks.json は、プロジェクトが使うmodel taskと、それが呼んでよいLLMのプロバイダーを列挙します。mockプロファイルではプロバイダーの代わりにmockが答えるので、まだAPIキーは要りません。

{
  "version": "actql-model-tasks/0.1",
  "tasks": {
    "model-task:churn-risk.v1": "./model-tasks/churn-risk.v1.yaml"
  },
  "providers": {
    "openai": {"type": "openai", "api_key_env": "OPENAI_API_KEY"}
  },
  "default_provider": "openai"
}

model-tasks/churn-risk.v1.yaml は、LLMに何を頼み、どんな型で答えを受け取るかを定義します。

task: model-task:churn-risk.v1
input:
  notes: string
  active_users: int64
  trend: string
instructions: |
  From the account notes, the number of active users and the usage trend,
  rate the churn risk as low, medium or high and explain why in one line.
  Do not assume facts that are not in the input.
output:
  level: {type: string, enum: [low, medium, high]}
  reason: string

mock/data.json は、mockのConnectorが返すレコードです。Salesforceの2件の取引先とその現在の値、そしてSnowflakeの利用データが入っています。key は、本物のConnectorのCatalogと同じように、各読み取り元のキーを宣言します。

{
  "salesforce": {
    "Account": {
      "key": ["id"],
      "rows": [
        {"id": "001Acme", "name": "Acme Corp", "description": "Evaluating a competitor; usage down since June.",
         "Churn_Risk__c": "low", "Risk_Reason__c": null},
        {"id": "001Globex", "name": "Globex Inc", "description": "Several support tickets about the new pricing.",
         "Churn_Risk__c": "medium", "Risk_Reason__c": null}
      ]
    }
  },
  "snowflake": {
    "usage_30d": {
      "key": ["account_id"],
      "rows": [
        {"account_id": "001Acme", "active_users": 4, "trend": "down"},
        {"account_id": "001Globex", "active_users": 12, "trend": "flat"}
      ]
    }
  }
}

salesforce.accounts はこのAccountのレコードを読み、EFFECT UPDATE salesforce.Account はそこに書き込みます。本物のorgと同じです。

mock/churn-risk.v1.answers.yaml は、mockのLLMが項目のキーごとに返す答えです。どちらの取引先も high と判定されます。

task: model-task:churn-risk.v1
answers:
  default: { level: low, reason: "stable usage" }
  by_key:
    "001Acme":   { level: high, reason: "falling usage and a competitor evaluation" }
    "001Globex": { level: high, reason: "support tickets about pricing" }

churn-risk.aql がプログラムです。

SELECT a.id AS id, a.description AS notes,
       u.active_users, u.trend
FROM salesforce.accounts AS a
JOIN snowflake.usage_30d AS u ON a.id = u.account_id
|> INFER `model-task:churn-risk.v1`
     INPUT (notes, active_users, trend) AS risk
|> WHERE risk.outcome = 'ok' AND risk.value.level = 'high'
|> RESULT updates
|> EFFECT UPDATE salesforce.Account
     ID id SET (
       Churn_Risk__c = risk.value.level,
       Risk_Reason__c = risk.value.reason
     );

上から順に読めます。取引先と利用データをJOINし、取引先ごとにLLMに聞き、リスクの高いものだけを残し、それを updates というResultとして公開し、リスクを書き戻します。INFER には、取引先ごとに1レコードが必要です。id は salesforce.accounts のキー、account_id は snowflake.usage_30d のキーなので、JOINの後も各 id は1回しか現れません。check は宣言されたキーからこれを証明し、証明できなければプログラムを拒否します。

2. 検査する

check は、名前、型、プログラムが頼りにする定義を検査し、このHostでプログラムを実行できるかも報告します。束縛されていないパラメーターや登録されていないmodel taskがあれば、正しいが実行できない、という結果になります。データは読みません。

$ actql check churn-risk.aql
Check  ok
  Runnable  yes
  Sources
    salesforce.accounts   id, description                  read
    snowflake.usage_30d   account_id, active_users, trend  read
  Model task
    model-task:churn-risk.v1   input (notes, active_users, trend) → level, reason
  Keys
    id unique after JOIN   salesforce.accounts.id = snowflake.usage_30d.account_id
  Effect
    update salesforce.Account   Churn_Risk__c, Risk_Reason__c   write
  Permissions
    salesforce: accounts.read, Account.update
    snowflake:  usage_30d.read
  No data read.

わざと壊してみてください。risk.value.level = 'high' を = 1 に変えて、もう一度 check を実行します。名前の付いた理由と行番号とともに拒否されます。

$ actql check churn-risk.aql
Check  failed
  expression_type_mismatch   line 7
    risk.value.level = 1   level is STRING, got INT64

3. 計画を見る

explain は、各部分がどこで実行されるかを見せます。ここでもまだ何も読みません。

$ actql explain churn-risk.aql

ActQL EXPLAIN · static
Check: valid       Run: not started
Provider I/O: none

[1] READ  salesforce.accounts
    Site: connector:salesforce
    Interface: Salesforce API
    Projection: id, description
    Pushdown: selected

[2] READ  snowflake.usage_30d
    Site: connector:snowflake
    Engine: Snowflake SQL
    Projection: account_id, active_users, trend
    Pushdown: selected

[3] JOIN  [1] + [2]
    Condition: a.id = u.account_id
    Site: host:local (fixed)
    Engine: deferred
      Candidates: ActQL RelA, DuckDB
    Input rows: unknown
    Output limit: 50,000 (Host policy)

[4] INFER  churn-risk.v1
    Input: [3]
    Site: model provider
    Calls: one per row of [3]; count unknown

[5] WHERE  risk.outcome = 'ok'
           AND risk.value.level = 'high'
    Depends on: [4]
    Site: host:local
    Engine: deferred
    Pushdown: not eligible
      Reason: depends on INFER output

[6] EFFECT UPDATE  salesforce.Account
    Site: connector:salesforce
    Preview: unavailable before execution
    Policy: evaluated at runtime
    Checkpoint: conditional

Estimates: unavailable (no statistics)
Details: actql explain churn-risk.aql --verbose

上から順に読むと、次のことが分かります。

  • それぞれの読み取り元には、プログラムが使う列だけを要求します。
  • JOINは手元のマシンで実行します(このプロジェクトはローカルのHostで動くので、host:local は確定です)。どのエンジンで計算するかは、件数が分かってからRunの中で決まるので、deferred と表示されます。
  • WHERE はLLMの答えを読むので、SalesforceにもSnowflakeにも送れません。INFER の後で実行します。
  • 更新は承認のために止まることがあります。止まるかどうかはRunがそこに来たときにポリシーが決めるので、Checkpointは conditional です。

各項目の意味、--verbose、Runの後の数字はPlanとプッシュダウンで説明しています。

4. 実行する

run は、データを読み、LLMに聞き、書き込む前に止まります。--stop-before commit を付けると、ポリシーが書き込みを通す設定でも必ず止まるので、Checkpointがどう見えるかを確かめられます。

$ actql run churn-risk.aql --stop-before commit
Run run_…
  Status      waiting_checkpoint
  Checkpoint  ckpt_…
  Effect      update  (Salesforce Account)
  Items       2
  Changes
    Acme Corp     Churn_Risk__c   low     → high
                  Risk_Reason__c  (empty) → falling usage and a competitor evaluation
    Globex Inc    Churn_Risk__c   medium  → high
                  Risk_Reason__c  (empty) → support tickets about pricing

Runは永続的です。ターミナルを閉じても、マシンを再起動しても、戻ってきて actql status run_… を実行すれば同じCheckpointが見えます。まだ何も書き込まれていません。並んでいる変更が、これから書き込まれる内容そのものです。mock/data.json の2件の取引先の2つの項目で、mockの答えでどちらも high と判定されたものです。

5. 承認して終える

$ actql approve ckpt_…
Checkpoint ckpt_…  approved
Run run_…  resuming

$ actql status run_…
Run run_…
  Status   succeeded
  Effect   update  (Salesforce Account)   2 items
    Acme Corp     applied
    Globex Inc    applied
  Results  updates  2 rows

$ actql result run_… --result updates --format jsonl
{"id":"001Acme","notes":"Evaluating a competitor; usage down since June.","active_users":4,"trend":"down","risk":{"outcome":"ok","value":{"level":"high","reason":"falling usage and a competitor evaluation"}}}
{"id":"001Globex","notes":"Several support tickets about the new pricing.","active_users":12,"trend":"flat","risk":{"outcome":"ok","value":{"level":"high","reason":"support tickets about pricing"}}}

承認すると、同じRunが続きます。すでに読んだデータとLLMの答えは再利用され、取り直されることはありません。status には書き込みごとの結果が出て、result はプログラムが公開した updates のResult、つまり書き込みの元になったレコードを読みます。承認せずに却下するなら actql reject ckpt_… です。Runは何も書かずに cancelled で終わります。

6. アプリから同じことをする

CLIは、Runを動かす方法の1つにすぎません。アプリからも @actql/node やPythonの actql パッケージで同じことができ、動かしておくサーバーはありません。

TypeScript:

import {readFile} from 'node:fs/promises';
import {connect} from '@actql/node';

const client = await connect({mode: 'local', installation: {directory: '.actql'}, profile: 'mock'});
try {
  const source = await readFile('churn-risk.aql', 'utf8');
  const execution = await client.startSource(source, {format: 'aql', stopBefore: 'commit'});
  const state = await execution.wait();
  console.log(state.status); // 'waiting_checkpoint'
  await client.approve(state.checkpoint.id);
  console.log((await execution.wait()).status); // 'succeeded'
} finally {
  await client.close();
}

Python:

import actql

with actql.connect(mode="local", installation=".actql", profile="mock") as client:
    source = open("churn-risk.aql").read()
    execution = client.start_source(source, format="aql", stop_before="commit")
    state = execution.wait()
    print(state.status)  # "waiting_checkpoint"
    client.approve(state.checkpoint.id)
    print(execution.wait().status)  # "succeeded"

どちらも新しいRunを始めるので、Checkpointもそれぞれ別です。CLI、TypeScript、Pythonは同じRunのモデルを使い、同じ .actql ディレクトリを指せば同じRunを扱えます。コードから始めたRunを、Run IDを指定してCLIで確認・承認することも、その逆もできます。

次に読む

  • プログラムを本物のシステムに向ける:ローカルとリモートのHostで接続、認証情報、ポリシーを、Connectorの各ページで各システムが公開しているものを説明しています。
  • 誰が何を承認するかを決める:Effectと承認
  • 自分のプログラムを書く:プログラムを書く
  • コーディングエージェントにActQLを渡す:npx skills add actql/skills のあと、「Start ActQL setup」と頼んでください。

Source: actql/actql/docs/getting-started/quickstart.md at d904416674e4 · Edit on GitHub