openclaw-contextualai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/biome.json +77 -0
- package/client.ts +137 -0
- package/commands/slash.ts +37 -0
- package/config.ts +78 -0
- package/hooks/before_agent_start.ts +23 -0
- package/index.ts +43 -0
- package/openclaw.plugin.json +53 -0
- package/package.json +24 -0
- package/tools/contextualai_list_agents.ts +62 -0
- package/tools/contextualai_lookup.ts +44 -0
- package/tools/contextualai_query_agent.ts +81 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI - Type Check, Format & Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
quality-checks:
|
|
8
|
+
name: Quality Checks
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Checkout code
|
|
12
|
+
uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Setup Node
|
|
15
|
+
uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: "20"
|
|
18
|
+
cache: "npm"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: npm ci
|
|
22
|
+
|
|
23
|
+
- name: Setup Biome
|
|
24
|
+
uses: biomejs/setup-biome@v2
|
|
25
|
+
with:
|
|
26
|
+
version: 2.3.8
|
|
27
|
+
|
|
28
|
+
- name: Run TypeScript type checking
|
|
29
|
+
run: npm run check-types
|
|
30
|
+
|
|
31
|
+
- name: Run Biome CI (format & lint)
|
|
32
|
+
run: npx biome ci .
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contextual AI Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Connect OpenClaw to [Contextual AI](https://contextual.ai) agents: list your agents and let the LLM route user questions to the right agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install openclaw-contextualai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Restart OpenClaw after installing.
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
The only required value is your Contextual AI API key. Get one at [app.contextual.ai](https://app.contextual.ai) (API Keys in the dashboard).
|
|
16
|
+
|
|
17
|
+
**Option A – Environment variable:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export OPENCLAW_CONTEXTUALAI_API_KEY="your-contextual-ai-api-key"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Option B – In `openclaw.json`:**
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"plugins": {
|
|
28
|
+
"entries": {
|
|
29
|
+
"openclaw-contextualai": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"config": {
|
|
32
|
+
"apiKey": "your-contextual-ai-api-key"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How to use
|
|
41
|
+
|
|
42
|
+
In any channel where OpenClaw is running:
|
|
43
|
+
|
|
44
|
+
- **List your agents** – e.g. *“What Contextual AI agents do I have?”* or *“List my Contextual AI agents.”*
|
|
45
|
+
- **Query an agent** – e.g. *“Ask the Materials Science Agent: What materials are best for high-temperature aerospace?”*
|
|
46
|
+
|
|
47
|
+
The model will list your agents, then send your question to the right one and reply with the agent’s answer.
|
|
48
|
+
|
|
49
|
+
## AI Tools
|
|
50
|
+
|
|
51
|
+
The AI can use these tools during conversations:
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `contextualai_list_agents` | List available Contextual AI agents (id, name, description). |
|
|
56
|
+
| `contextualai_query_agent` | Send a message to a specific agent by id and return its response. |
|
|
57
|
+
|
|
58
|
+
## Optional config
|
|
59
|
+
|
|
60
|
+
| Key | Type | Default | Description |
|
|
61
|
+
|----------|---------|-----------------------------|---------------------------------------------|
|
|
62
|
+
| `baseUrl`| string | `https://api.contextual.ai` | API base URL. |
|
|
63
|
+
| `debug` | boolean | false | Log request URL and response status. |
|
|
64
|
+
|
|
65
|
+
Env vars: `OPENCLAW_CONTEXTUALAI_BASE_URL`, `OPENCLAW_CONTEXTUALAI_API_KEY` (or set `apiKey` in config).
|
package/biome.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
|
3
|
+
"assist": {
|
|
4
|
+
"actions": {
|
|
5
|
+
"source": {
|
|
6
|
+
"organizeImports": "on",
|
|
7
|
+
"useSortedAttributes": "on",
|
|
8
|
+
"useSortedKeys": "off"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"enabled": true
|
|
12
|
+
},
|
|
13
|
+
"files": {
|
|
14
|
+
"includes": [
|
|
15
|
+
"**",
|
|
16
|
+
"!**/node_modules",
|
|
17
|
+
"!**/dist",
|
|
18
|
+
"!**/bun.lock",
|
|
19
|
+
"!**/*.lock"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"formatter": {
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"indentStyle": "tab"
|
|
25
|
+
},
|
|
26
|
+
"javascript": {
|
|
27
|
+
"formatter": {
|
|
28
|
+
"quoteStyle": "double",
|
|
29
|
+
"semicolons": "asNeeded"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"linter": {
|
|
33
|
+
"domains": {
|
|
34
|
+
"project": "none"
|
|
35
|
+
},
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"rules": {
|
|
38
|
+
"correctness": {
|
|
39
|
+
"useYield": "warn",
|
|
40
|
+
"noUnusedVariables": {
|
|
41
|
+
"level": "warn",
|
|
42
|
+
"options": {
|
|
43
|
+
"ignoreRestSiblings": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"noUnusedImports": "warn",
|
|
47
|
+
"useParseIntRadix": "off"
|
|
48
|
+
},
|
|
49
|
+
"recommended": true,
|
|
50
|
+
"style": {
|
|
51
|
+
"noDefaultExport": "off",
|
|
52
|
+
"noInferrableTypes": "error",
|
|
53
|
+
"noNonNullAssertion": "warn",
|
|
54
|
+
"noParameterAssign": "error",
|
|
55
|
+
"noUnusedTemplateLiteral": "error",
|
|
56
|
+
"noUselessElse": "error",
|
|
57
|
+
"useAsConstAssertion": "error",
|
|
58
|
+
"useDefaultParameterLast": "error",
|
|
59
|
+
"useEnumInitializers": "error",
|
|
60
|
+
"useNamingConvention": {
|
|
61
|
+
"level": "off",
|
|
62
|
+
"options": {
|
|
63
|
+
"strictCase": false
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"useNumberNamespace": "error",
|
|
67
|
+
"useSelfClosingElements": "error",
|
|
68
|
+
"useSingleVarDeclarator": "error"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"vcs": {
|
|
73
|
+
"clientKind": "git",
|
|
74
|
+
"enabled": true,
|
|
75
|
+
"useIgnoreFile": true
|
|
76
|
+
}
|
|
77
|
+
}
|
package/client.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ContextualAiConfig } from "./config.ts"
|
|
2
|
+
|
|
3
|
+
export type AgentEntry = {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ListAgentsResult = {
|
|
10
|
+
agents: AgentEntry[]
|
|
11
|
+
next_cursor?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type MessageRole = {
|
|
15
|
+
role: "user" | "assistant"
|
|
16
|
+
content: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type QueryAgentResult = {
|
|
20
|
+
content: string
|
|
21
|
+
conversation_id?: string
|
|
22
|
+
message_id?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeBaseUrl(baseUrl: string): string {
|
|
26
|
+
const trimmed = baseUrl.replace(/\/+$/, "")
|
|
27
|
+
if (trimmed.endsWith("/v1")) return trimmed
|
|
28
|
+
return `${trimmed}/v1`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const API_KEY_REQUIRED_MSG =
|
|
32
|
+
"openclaw-contextualai: apiKey is required (set in plugin config or OPENCLAW_CONTEXTUALAI_API_KEY env var)"
|
|
33
|
+
|
|
34
|
+
export class ContextualAiClient {
|
|
35
|
+
private apiKey: string
|
|
36
|
+
private baseUrl: string
|
|
37
|
+
private debug: boolean
|
|
38
|
+
|
|
39
|
+
constructor(cfg: Pick<ContextualAiConfig, "apiKey" | "baseUrl" | "debug">) {
|
|
40
|
+
this.apiKey = cfg.apiKey ?? ""
|
|
41
|
+
this.baseUrl = normalizeBaseUrl(cfg.baseUrl)
|
|
42
|
+
this.debug = cfg.debug ?? false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private ensureApiKey(): void {
|
|
46
|
+
if (!this.apiKey || this.apiKey.length === 0) {
|
|
47
|
+
throw new Error(API_KEY_REQUIRED_MSG)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async request<T>(
|
|
52
|
+
method: string,
|
|
53
|
+
path: string,
|
|
54
|
+
body?: unknown,
|
|
55
|
+
): Promise<T> {
|
|
56
|
+
const url = `${this.baseUrl}${path}`
|
|
57
|
+
if (this.debug) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.debug(`[contextualai] ${method} ${url}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const headers: Record<string, string> = {
|
|
63
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
64
|
+
}
|
|
65
|
+
if (body !== undefined) {
|
|
66
|
+
headers["Content-Type"] = "application/json"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const res = await fetch(url, {
|
|
70
|
+
method,
|
|
71
|
+
headers,
|
|
72
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (this.debug) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.debug(`[contextualai] ${res.status} ${path}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const text = await res.text()
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Contextual AI API error ${res.status}: ${text || res.statusText}`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (text.length === 0) {
|
|
88
|
+
return undefined as T
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(text) as T
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error(`Contextual AI API invalid JSON: ${text.slice(0, 200)}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async listAgents(limit = 100, cursor?: string): Promise<ListAgentsResult> {
|
|
98
|
+
this.ensureApiKey()
|
|
99
|
+
const params = new URLSearchParams()
|
|
100
|
+
params.set("limit", String(limit))
|
|
101
|
+
if (cursor) params.set("cursor", cursor)
|
|
102
|
+
const path = `/agents?${params.toString()}`
|
|
103
|
+
const data = await this.request<{ next_cursor?: string; agents?: AgentEntry[] }>(
|
|
104
|
+
"GET",
|
|
105
|
+
path,
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
agents: data.agents ?? [],
|
|
109
|
+
next_cursor: data.next_cursor,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async queryAgent(
|
|
114
|
+
agentId: string,
|
|
115
|
+
messages: MessageRole[],
|
|
116
|
+
conversationId?: string,
|
|
117
|
+
): Promise<QueryAgentResult> {
|
|
118
|
+
this.ensureApiKey()
|
|
119
|
+
const body: { messages: MessageRole[]; conversation_id?: string } = {
|
|
120
|
+
messages,
|
|
121
|
+
}
|
|
122
|
+
if (conversationId) body.conversation_id = conversationId
|
|
123
|
+
|
|
124
|
+
const path = `/agents/${encodeURIComponent(agentId)}/query`
|
|
125
|
+
const data = await this.request<{
|
|
126
|
+
conversation_id?: string
|
|
127
|
+
message_id?: string
|
|
128
|
+
message?: { content?: string; role?: string }
|
|
129
|
+
}>("POST", path, body)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: data.message?.content ?? "",
|
|
133
|
+
conversation_id: data.conversation_id,
|
|
134
|
+
message_id: data.message_id,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import type { ContextualAiConfig } from "../config.ts"
|
|
3
|
+
|
|
4
|
+
export function registerCommands(
|
|
5
|
+
api: OpenClawPluginApi,
|
|
6
|
+
_cfg: ContextualAiConfig,
|
|
7
|
+
): void {
|
|
8
|
+
api.registerCommand({
|
|
9
|
+
name: "contextai",
|
|
10
|
+
description: "Inspect or test Contextual AI configuration and behavior.",
|
|
11
|
+
acceptsArgs: true,
|
|
12
|
+
requireAuth: true,
|
|
13
|
+
async handler(ctx: { args?: string }) {
|
|
14
|
+
const subcommand = ctx.args?.trim() || "help"
|
|
15
|
+
|
|
16
|
+
if (subcommand === "help") {
|
|
17
|
+
return {
|
|
18
|
+
text:
|
|
19
|
+
"/contextai commands:\n" +
|
|
20
|
+
"- /contextai help Show this help.\n" +
|
|
21
|
+
"- /contextai ping Check that the plugin is wired correctly.\n",
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (subcommand === "ping") {
|
|
26
|
+
return {
|
|
27
|
+
text: "openclaw-contextualai: plugin is installed and responding.",
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
text: `Unknown subcommand: "${subcommand}". Try /contextai help.`,
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
package/config.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type ContextualAiConfig = {
|
|
2
|
+
apiKey: string
|
|
3
|
+
baseUrl: string
|
|
4
|
+
projectId?: string
|
|
5
|
+
autoInject: boolean
|
|
6
|
+
maxContextTokens: number
|
|
7
|
+
debug: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ALLOWED_KEYS = [
|
|
11
|
+
"apiKey",
|
|
12
|
+
"baseUrl",
|
|
13
|
+
"projectId",
|
|
14
|
+
"autoInject",
|
|
15
|
+
"maxContextTokens",
|
|
16
|
+
"debug",
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
function assertAllowedKeys(
|
|
20
|
+
value: Record<string, unknown>,
|
|
21
|
+
allowed: readonly string[],
|
|
22
|
+
label: string,
|
|
23
|
+
): void {
|
|
24
|
+
const unknown = Object.keys(value).filter((k) => !allowed.includes(k))
|
|
25
|
+
if (unknown.length > 0) {
|
|
26
|
+
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveEnvVars(value: string): string {
|
|
31
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, envVar: string) => {
|
|
32
|
+
const envValue = process.env[envVar]
|
|
33
|
+
if (!envValue) {
|
|
34
|
+
throw new Error(`Environment variable ${envVar} is not set`)
|
|
35
|
+
}
|
|
36
|
+
return envValue
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseConfig(raw: unknown): ContextualAiConfig {
|
|
41
|
+
const cfg =
|
|
42
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
43
|
+
? (raw as Record<string, unknown>)
|
|
44
|
+
: {}
|
|
45
|
+
|
|
46
|
+
if (Object.keys(cfg).length > 0) {
|
|
47
|
+
assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-contextualai config")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const apiKeyValue = cfg.apiKey ?? process.env.OPENCLAW_CONTEXTUALAI_API_KEY
|
|
51
|
+
const baseUrlValue =
|
|
52
|
+
cfg.baseUrl ?? process.env.OPENCLAW_CONTEXTUALAI_BASE_URL ?? "https://api.contextual.ai"
|
|
53
|
+
|
|
54
|
+
// Allow missing key at startup so the gateway can start; the client will throw when a tool is used.
|
|
55
|
+
const apiKey =
|
|
56
|
+
apiKeyValue && typeof apiKeyValue === "string" && apiKeyValue.length > 0
|
|
57
|
+
? resolveEnvVars(apiKeyValue)
|
|
58
|
+
: ""
|
|
59
|
+
|
|
60
|
+
const baseUrl =
|
|
61
|
+
typeof baseUrlValue === "string" && baseUrlValue.length > 0
|
|
62
|
+
? resolveEnvVars(baseUrlValue)
|
|
63
|
+
: "https://api.contextual.ai"
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
apiKey,
|
|
67
|
+
baseUrl,
|
|
68
|
+
projectId: typeof cfg.projectId === "string" ? cfg.projectId : undefined,
|
|
69
|
+
autoInject: (cfg.autoInject as boolean) ?? true,
|
|
70
|
+
maxContextTokens: (cfg.maxContextTokens as number) ?? 4096,
|
|
71
|
+
debug: (cfg.debug as boolean) ?? false,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const contextualAiConfigSchema = {
|
|
76
|
+
parse: parseConfig,
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import type { ContextualAiConfig } from "../config.ts"
|
|
3
|
+
|
|
4
|
+
export function registerBeforeAgentStartHook(
|
|
5
|
+
api: OpenClawPluginApi,
|
|
6
|
+
_cfg: ContextualAiConfig,
|
|
7
|
+
): void {
|
|
8
|
+
api.on(
|
|
9
|
+
"before_agent_start",
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
async (event: Record<string, any>, _ctx: Record<string, any>) => {
|
|
12
|
+
// TODO: use your contextual backend + event/ctx to fetch relevant context.
|
|
13
|
+
// For now, we just return the original event without modifications.
|
|
14
|
+
|
|
15
|
+
// Example of how you might eventually inject context:
|
|
16
|
+
// const extraContext = await fetchContextFromBackend(event, ctx, cfg)
|
|
17
|
+
// return { ...event, prependContext: extraContext }
|
|
18
|
+
|
|
19
|
+
return event
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import { ContextualAiClient } from "./client.ts"
|
|
3
|
+
import { parseConfig, contextualAiConfigSchema } from "./config.ts"
|
|
4
|
+
import { registerBeforeAgentStartHook } from "./hooks/before_agent_start.ts"
|
|
5
|
+
import { registerCommands } from "./commands/slash.ts"
|
|
6
|
+
import { registerListAgentsTool } from "./tools/contextualai_list_agents.ts"
|
|
7
|
+
import { registerQueryAgentTool } from "./tools/contextualai_query_agent.ts"
|
|
8
|
+
import { registerContextualLookupTool } from "./tools/contextualai_lookup.ts"
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
id: "openclaw-contextualai",
|
|
12
|
+
name: "Contextual AI",
|
|
13
|
+
description: "Context-aware extension for OpenClaw that injects relevant context into conversations.",
|
|
14
|
+
kind: "extension" as const,
|
|
15
|
+
configSchema: contextualAiConfigSchema,
|
|
16
|
+
|
|
17
|
+
register(api: OpenClawPluginApi) {
|
|
18
|
+
const cfg = parseConfig(api.pluginConfig)
|
|
19
|
+
const client = new ContextualAiClient(cfg)
|
|
20
|
+
|
|
21
|
+
// Register tools: list agents and query agent (Step 1 – talk to Contextual AI agents directly).
|
|
22
|
+
registerListAgentsTool(api, client, cfg)
|
|
23
|
+
registerQueryAgentTool(api, client, cfg)
|
|
24
|
+
registerContextualLookupTool(api, cfg)
|
|
25
|
+
|
|
26
|
+
// Register hooks to inject context before agent runs.
|
|
27
|
+
registerBeforeAgentStartHook(api, cfg)
|
|
28
|
+
|
|
29
|
+
// Register basic slash commands for debugging / inspection.
|
|
30
|
+
registerCommands(api, cfg)
|
|
31
|
+
|
|
32
|
+
api.registerService({
|
|
33
|
+
id: "openclaw-contextualai",
|
|
34
|
+
start: () => {
|
|
35
|
+
api.logger.info("openclaw-contextualai: connected")
|
|
36
|
+
},
|
|
37
|
+
stop: () => {
|
|
38
|
+
api.logger.info("openclaw-contextualai: stopped")
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-contextualai",
|
|
3
|
+
"kind": "extension",
|
|
4
|
+
"uiHints": {
|
|
5
|
+
"apiKey": {
|
|
6
|
+
"label": "Contextual AI API Key",
|
|
7
|
+
"sensitive": true,
|
|
8
|
+
"placeholder": "${OPENCLAW_CONTEXTUALAI_API_KEY}",
|
|
9
|
+
"help": "API key for your Contextual AI backend. You can also set it via the OPENCLAW_CONTEXTUALAI_API_KEY environment variable."
|
|
10
|
+
},
|
|
11
|
+
"baseUrl": {
|
|
12
|
+
"label": "Base URL",
|
|
13
|
+
"placeholder": "https://api.contextual.ai",
|
|
14
|
+
"help": "Base URL for the Contextual AI service.",
|
|
15
|
+
"advanced": true
|
|
16
|
+
},
|
|
17
|
+
"projectId": {
|
|
18
|
+
"label": "Project ID",
|
|
19
|
+
"placeholder": "optional-project-id",
|
|
20
|
+
"help": "Optional project or workspace identifier used by your contextual backend.",
|
|
21
|
+
"advanced": true
|
|
22
|
+
},
|
|
23
|
+
"autoInject": {
|
|
24
|
+
"label": "Auto Inject Context",
|
|
25
|
+
"help": "Automatically fetch and inject relevant contextual information before every AI turn."
|
|
26
|
+
},
|
|
27
|
+
"maxContextTokens": {
|
|
28
|
+
"label": "Max Context Tokens",
|
|
29
|
+
"placeholder": "4096",
|
|
30
|
+
"help": "Maximum number of tokens of contextual information to inject into each turn.",
|
|
31
|
+
"advanced": true
|
|
32
|
+
},
|
|
33
|
+
"debug": {
|
|
34
|
+
"label": "Debug Logging",
|
|
35
|
+
"help": "Enable verbose debug logs for contextual requests and responses.",
|
|
36
|
+
"advanced": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"configSchema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"properties": {
|
|
43
|
+
"apiKey": { "type": "string" },
|
|
44
|
+
"baseUrl": { "type": "string" },
|
|
45
|
+
"projectId": { "type": "string" },
|
|
46
|
+
"autoInject": { "type": "boolean" },
|
|
47
|
+
"maxContextTokens": { "type": "number", "minimum": 512, "maximum": 32768 },
|
|
48
|
+
"debug": { "type": "boolean" }
|
|
49
|
+
},
|
|
50
|
+
"required": []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-contextualai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Contextual AI extension for OpenClaw",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check-types": "tsc --noEmit",
|
|
9
|
+
"lint": "bunx @biomejs/biome ci .",
|
|
10
|
+
"lint:fix": "bunx @biomejs/biome check --write ."
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"openclaw": ">=2026.1.29"
|
|
14
|
+
},
|
|
15
|
+
"openclaw": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./index.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@sinclair/typebox": "0.34.47",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { ContextualAiClient } from "../client.ts"
|
|
4
|
+
import type { ContextualAiConfig } from "../config.ts"
|
|
5
|
+
|
|
6
|
+
export function registerListAgentsTool(
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
client: ContextualAiClient,
|
|
9
|
+
_cfg: ContextualAiConfig,
|
|
10
|
+
): void {
|
|
11
|
+
api.registerTool(
|
|
12
|
+
{
|
|
13
|
+
name: "contextualai_list_agents",
|
|
14
|
+
label: "List Contextual AI Agents",
|
|
15
|
+
description:
|
|
16
|
+
"Returns the list of Contextual AI agents available to query. Use this to see agent id, name, and description so you can choose the right agent and call contextualai_query_agent with its id.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
limit: Type.Optional(
|
|
19
|
+
Type.Number({
|
|
20
|
+
description: "Maximum number of agents to return (default 50)",
|
|
21
|
+
default: 50,
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
}),
|
|
25
|
+
async execute(_toolCallId: string, params: { limit?: number }) {
|
|
26
|
+
const limit = params.limit ?? 50
|
|
27
|
+
const result = await client.listAgents(limit)
|
|
28
|
+
|
|
29
|
+
if (result.agents.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text" as const,
|
|
34
|
+
text: "No Contextual AI agents found.",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
details: { agents: [], count: 0 },
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = result.agents.map(
|
|
42
|
+
(a) => `- **${a.name}** (id: \`${a.id}\`): ${a.description ?? ""}`,
|
|
43
|
+
)
|
|
44
|
+
const text =
|
|
45
|
+
`Found ${result.agents.length} agent(s):\n\n${lines.join("\n")}` +
|
|
46
|
+
(result.next_cursor
|
|
47
|
+
? "\n\n(More agents available; use limit to fetch more.)"
|
|
48
|
+
: "")
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text" as const, text }],
|
|
52
|
+
details: {
|
|
53
|
+
agents: result.agents,
|
|
54
|
+
count: result.agents.length,
|
|
55
|
+
next_cursor: result.next_cursor,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ name: "contextualai_list_agents" },
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { ContextualAiConfig } from "../config.ts"
|
|
4
|
+
|
|
5
|
+
export function registerContextualLookupTool(
|
|
6
|
+
api: OpenClawPluginApi,
|
|
7
|
+
_cfg: ContextualAiConfig,
|
|
8
|
+
): void {
|
|
9
|
+
api.registerTool(
|
|
10
|
+
{
|
|
11
|
+
name: "contextualai_lookup",
|
|
12
|
+
label: "Contextual Lookup",
|
|
13
|
+
description:
|
|
14
|
+
"Fetches contextual information relevant to the current conversation or query.",
|
|
15
|
+
parameters: Type.Object({
|
|
16
|
+
query: Type.String({
|
|
17
|
+
description:
|
|
18
|
+
"Natural language description of the context the AI wants to retrieve.",
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
// Note: this is a stub implementation – wire it to your backend later.
|
|
22
|
+
async execute(_toolCallId: string, params: { query: string }) {
|
|
23
|
+
const preview =
|
|
24
|
+
params.query.length > 120
|
|
25
|
+
? `${params.query.slice(0, 120)}…`
|
|
26
|
+
: params.query
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text" as const,
|
|
32
|
+
text:
|
|
33
|
+
`[contextualai_lookup] This is a placeholder response for query: "${preview}". ` +
|
|
34
|
+
"Wire this tool up to your contextual backend to return real context snippets.",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
details: {},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{ name: "contextualai_lookup" },
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { ContextualAiClient } from "../client.ts"
|
|
4
|
+
import type { ContextualAiConfig } from "../config.ts"
|
|
5
|
+
|
|
6
|
+
export function registerQueryAgentTool(
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
client: ContextualAiClient,
|
|
9
|
+
_cfg: ContextualAiConfig,
|
|
10
|
+
): void {
|
|
11
|
+
api.registerTool(
|
|
12
|
+
{
|
|
13
|
+
name: "contextualai_query_agent",
|
|
14
|
+
label: "Query Contextual AI Agent",
|
|
15
|
+
description:
|
|
16
|
+
"Send a message to a specific Contextual AI agent by its id (from contextualai_list_agents) and get the agent's response. Use this to redirect user questions to the appropriate specialist agent.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
agent_id: Type.String({
|
|
19
|
+
description: "UUID of the agent to query (from contextualai_list_agents)",
|
|
20
|
+
}),
|
|
21
|
+
message: Type.String({
|
|
22
|
+
description: "The user message or question to send to the agent",
|
|
23
|
+
}),
|
|
24
|
+
conversation_id: Type.Optional(
|
|
25
|
+
Type.String({
|
|
26
|
+
description:
|
|
27
|
+
"Optional conversation id for follow-up messages in the same thread",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
}),
|
|
31
|
+
async execute(
|
|
32
|
+
_toolCallId: string,
|
|
33
|
+
params: { agent_id: string; message: string; conversation_id?: string },
|
|
34
|
+
) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await client.queryAgent(
|
|
37
|
+
params.agent_id,
|
|
38
|
+
[{ role: "user", content: params.message }],
|
|
39
|
+
params.conversation_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (!result.content) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text" as const,
|
|
47
|
+
text: "The agent returned an empty response.",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
details: {
|
|
51
|
+
conversation_id: result.conversation_id,
|
|
52
|
+
message_id: result.message_id,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text" as const, text: result.content }],
|
|
59
|
+
details: {
|
|
60
|
+
conversation_id: result.conversation_id,
|
|
61
|
+
message_id: result.message_id,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const message =
|
|
66
|
+
err instanceof Error ? err.message : String(err)
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text" as const,
|
|
71
|
+
text: `Contextual AI query failed: ${message}`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
details: {},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{ name: "contextualai_query_agent" },
|
|
80
|
+
)
|
|
81
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"rootDir": "."
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"*.ts",
|
|
17
|
+
"tools/*.ts",
|
|
18
|
+
"hooks/*.ts",
|
|
19
|
+
"commands/*.ts",
|
|
20
|
+
"types/*.d.ts"
|
|
21
|
+
],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|