クイックスタート
解約リスクの例をmockのConnectorで動かし、検査、計画、Checkpointでの停止、承認、Resultの読み取りまでを一通り試す。
10分で、1つのプログラムを動かします。Salesforceから取引先を、Snowflakeから利用データを読み、どの取引先に解約リスクがあるかをLLMに判定させ、承認のために止まり、答えをSalesforceに書き戻すプログラムです。相手はすべてmockのシステムなので、本物には何も触れず、認証情報も要りません。まずCLIから、次にTypeScriptとPythonから動かします。
先にCLIをインストールしてください(インストール)。
npm install -g actql1. プロジェクト
プロジェクトはCLIに作らせます。
actql init --template churn-risk-mock churn-risk
cd churn-riskinit は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: stringmock/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 INT643. 計画を見る
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 pricingRunは永続的です。ターミナルを閉じても、マシンを再起動しても、戻ってきて 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