Skip to content

Commit 0efb8de

Browse files
committed
- Introduced the smart-completions module, including core classes such as SmartCompletions and SmartCompletion for managing completion requests and responses.
- Added various adapters for handling different completion scenarios, including `SmartCompletionTemplateAdapter`, `SmartCompletionContextAdapter`, and `ActionXmlCompletionAdapter`. - Created a default configuration for smart completions and established a structure for handling user messages and system prompts. - Included comprehensive documentation in `README.md` and `spec.md` to outline usage, key components, and integration details. - Maintained existing comments and methods unrelated to the changes.
1 parent fce0b7b commit 0efb8de

20 files changed

Lines changed: 1882 additions & 0 deletions

smart-completions/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Smart Completions
2+
3+
Smart Completions is a **smart-collections**-based module for managing completion requests/responses in a structured way. Each completion is a **SmartCompletion** item that can run one or more request adapters to transform or enrich the data before sending it to a chat model. Responses are then stored, making it easy to track history or retrieve final output.
4+
5+
## Key Components
6+
7+
- **SmartCompletions**
8+
A collection (extending `Collection` from `smart-collections`) that manages multiple completion items.
9+
10+
- Exposes a shared `chat_model` (if configured) for all items.
11+
- Provides `completion_adapters` to transform item data prior to sending a request.
12+
- **SmartCompletion**
13+
A single completion item (extending `CollectionItem`), storing:
14+
15+
- `completion.request`: Data in a chat-model-friendly format (e.g. OpenAI-like messages).
16+
- `completion.responses[]`: The returned data from the model.
17+
- `completion.chat_model`: (Optional) An override specifying a dedicated model instance.
18+
- Adapters can modify the request or the response.
19+
- **Adapters** (in `adapters/`)
20+
Small classes extending `SmartCompletionAdapter`. Each checks for certain properties in `item.data` (e.g. `user_message`, `context_key`, etc.) and transforms `item.data.completion.request` or processes the response.
21+
22+
23+
Examples:
24+
25+
- `SmartCompletionUserAdapter`: Appends a user message to `request.messages`.
26+
- `SmartCompletionContextAdapter`: Merges ephemeral context into `request.messages`.
27+
- `SmartCompletionTemplateAdapter`: Injects template instructions.
28+
- `ThreadCompletionAdapter`: Appends conversation history.
29+
30+
## Basic Usage
31+
32+
1. **Register the collection** in your environment config:
33+
34+
```
35+
{
36+
collections: {
37+
smart_completions: {
38+
class: SmartCompletions
39+
// optional data_adapter, etc.
40+
}
41+
},
42+
modules: {
43+
smart_chat_model: {
44+
// chat model class & adapters
45+
}
46+
}
47+
}
48+
```
49+
50+
1. **Create or update** a completion item:
51+
52+
```
53+
const completions = env.smart_completions;
54+
const item = completions.create_or_update({
55+
key: 'my_completion_1',
56+
data: {
57+
user_message: 'Hello AI, how are you?',
58+
completion: {
59+
request: {
60+
messages: []
61+
}
62+
// optional chat_model overrides
63+
}
64+
}
65+
});
66+
```
67+
68+
- Once created, the item calls its `init()` method, which:
69+
- Runs each adapter (e.g. user adapter adds a user message).
70+
- Calls the chat model to get a response.
71+
- Stores the response in `completion.responses`.
72+
73+
1. **Get the final text**:
74+
75+
```
76+
console.log(item.response_text);
77+
```
78+
79+
- This looks for the first choice returned by the model (e.g. `.choices[0].message.content`).
80+
81+
## Adapters Flow
82+
83+
Each adapter implements:
84+
85+
- `to_request()`: Reads or transforms `item.data` → modifies `completion.request`.
86+
- `from_response()`: (Optional) Processes the final response. For example, parse out new properties or clean sensitive data.
87+
88+
When `item.init()` runs, it calls `build_request()` which invokes `to_request()` in every relevant adapter. After calling the model, you can also trigger `from_response()` if needed (though most adapters simply rely on `to_request()`).
89+
90+
## Custom Adapters
91+
92+
To add your own adapter:
93+
94+
1. Extend `SmartCompletionAdapter`.
95+
2. Implement `static get property_name()` returning the `item.data` property that triggers this adapter.
96+
3. Override `to_request()` (and optionally `from_response()`).
97+
98+
Register it in your environment config:
99+
100+
```
101+
{
102+
collections: {
103+
smart_completions: {
104+
class: SmartCompletions,
105+
completion_adapters: {
106+
MyCustomAdapter
107+
}
108+
}
109+
}
110+
}
111+
```
112+
113+
## Data Storage
114+
115+
By default, the code references `smart-collections/adapters/ajson_single_file.js` for storing items in an append-only JSON file. You can customize or swap out any data adapter.
116+
117+
## Example
118+
119+
```
120+
const completions = env.smart_completions;
121+
const item = completions.create_or_update({
122+
data: {
123+
user_message: "What's the weather?",
124+
completion: { request: { messages: [] } }
125+
}
126+
});
127+
console.log("Response text:", item.response_text);
128+
```
129+
130+
No separate `new_completion` method is strictly required; you can just use `create_or_update` as shown. The item will handle constructing the request (via adapters) and fetching the response.
131+
132+
## License
133+
134+
MIT
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { insert_user_message } from "../utils/insert_user_message.js";
2+
3+
/**
4+
* @class SmartCompletionAdapter
5+
* @extends SmartCompletionAdapter
6+
*
7+
* This adapter checks `item.data.user` and, if present, appends it as a user message
8+
* to `completion.request.messages`.
9+
*/
10+
export class SmartCompletionAdapter {
11+
constructor(item) {
12+
this.item = item;
13+
}
14+
get data () {
15+
return this.item.data;
16+
}
17+
get env () {
18+
return this.item.env;
19+
}
20+
get completion () {
21+
return this.data.completion;
22+
}
23+
get request () {
24+
return this.item.data.completion.request;
25+
}
26+
get response () {
27+
return this.item.response;
28+
}
29+
insert_user_message(user_message) {
30+
insert_user_message(this.request, user_message);
31+
}
32+
33+
// Override these methods in subclasses
34+
static get property_name() {
35+
return null;
36+
}
37+
/**
38+
* @returns {Promise<void>}
39+
*/
40+
async to_request() {}
41+
42+
/**
43+
* @returns {Promise<void>}
44+
*/
45+
async from_response() {}
46+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { SmartCompletionAdapter } from './_adapter.js';
2+
3+
/**
4+
* @class ActionCompletionAdapter
5+
* @extends SmartCompletionAdapter
6+
*
7+
* This adapter checks `item.data.action` as a single SmartAction key,
8+
* compiles ephemeral action text, and inserts it as a system message.
9+
* Then, after response, if the assistant includes `tool_call` in `this.response`,
10+
* it calls the corresponding `action_item.run(args)` and stores the result in
11+
* `this.data.actions[action_key]`.
12+
*/
13+
export class ActionCompletionAdapter extends SmartCompletionAdapter {
14+
static get property_name() {
15+
return 'action_key';
16+
}
17+
18+
/**
19+
* @returns {Promise<void>}
20+
*/
21+
async to_request() {
22+
const action_key = this.data.action_key;
23+
if (!action_key) return;
24+
25+
const action_opts = this.data.action_opts;
26+
27+
// Single context item only:
28+
const action_collection = this.item.env.smart_actions;
29+
if (!action_collection) {
30+
console.warn("No 'smart_actions' collection found; skipping action adapter.");
31+
return;
32+
}
33+
const action_item = action_collection.get(action_key);
34+
if (!action_item) {
35+
console.warn(`SmartAction not found for key '${action_key}'`);
36+
return;
37+
}
38+
39+
let tools;
40+
try {
41+
const action_module = action_item.module;
42+
tools = action_module.tool ? [action_module.tool] : convert_openapi_to_tools(action_module.openapi);
43+
} catch (err) {
44+
console.warn('Error compiling ephemeral action', err);
45+
return;
46+
}
47+
48+
if (!tools) return;
49+
50+
// Mark that this action is being requested (before completion)
51+
if (!this.data.actions) this.data.actions = {};
52+
this.data.actions[action_key] = true;
53+
54+
this.insert_tools(tools, { force: true });
55+
}
56+
57+
/**
58+
* @returns {Promise<void>}
59+
*/
60+
async from_response() {
61+
console.log('ActionCompletionAdapter: from_response');
62+
63+
const tool_call = this.response.choices[0].message?.tool_calls?.[0];
64+
if (!tool_call) return console.warn('No tool call found in response');
65+
const action_key = tool_call?.function?.name;
66+
const tool_arguments = tool_call?.function?.arguments;
67+
if (!action_key) return;
68+
69+
// Use the same collection from env
70+
const action_collection = this.item.env.smart_actions;
71+
if (!action_collection) return;
72+
73+
const action_item = action_collection.get(action_key);
74+
if (!action_item) return;
75+
76+
// Attempt to parse arguments if it's a string
77+
let parsed_args = tool_arguments;
78+
if (typeof parsed_args === 'string') {
79+
try {
80+
parsed_args = JSON.parse(parsed_args);
81+
} catch (err) {
82+
console.warn('Could not parse tool_call arguments', err);
83+
return;
84+
}
85+
}
86+
87+
// Run the action
88+
const result = await action_item.run_action(parsed_args);
89+
console.log('ActionCompletionAdapter: result', result);
90+
// If the tool returns an object with a 'final' key, treat it as the final assistant message
91+
if (result && typeof result === 'object' && result.final) {
92+
// Ensure we have a response object in completion
93+
if (!this.item.data.completion.responses[0]) {
94+
this.item.data.completion.responses[0] = { choices: [ { message: {} } ] };
95+
} else if (!this.item.data.completion.responses[0].choices?.[0]) {
96+
this.item.data.completion.responses[0].choices = [ { message: {} } ];
97+
}
98+
99+
// Write final as the assistant's content
100+
this.item.data.completion.responses[0].choices[0].message = {
101+
...this.item.data.completion.responses[0].choices[0].message,
102+
role: "assistant",
103+
content: result.final
104+
};
105+
}
106+
107+
// Store the result in `this.data.actions` under the same action key
108+
if (!this.data.actions) this.data.actions = {};
109+
this.data.actions[action_key] = result;
110+
}
111+
112+
/**
113+
* Insert the ephemeral tools into the request
114+
* @param {Array<object>} tools
115+
* @param {object} opts
116+
* @returns {void}
117+
*/
118+
insert_tools(tools, opts = {}) {
119+
this.request.tools = tools;
120+
if (opts.force) {
121+
this.request.tool_choice = {
122+
type: 'function',
123+
function: {
124+
name: tools[0].function.name
125+
}
126+
};
127+
}
128+
}
129+
}
130+
131+
/**
132+
* Converts OpenAPI specification into OpenAI's tool_call format
133+
* @param {object} openapi_spec - Parsed OpenAPI JSON specification
134+
* @returns {Array<object>} Array of tool_call formatted objects
135+
*/
136+
export function convert_openapi_to_tools(openapi_spec) {
137+
const tools = [];
138+
139+
for (const path in openapi_spec.paths) {
140+
const methods = openapi_spec.paths[path];
141+
142+
for (const method in methods) {
143+
const endpoint = methods[method];
144+
145+
const parameters = endpoint.parameters || [];
146+
const requestBody = endpoint.requestBody;
147+
148+
const properties = {};
149+
const required = [];
150+
151+
parameters.forEach(param => {
152+
properties[param.name] = {
153+
type: param.schema.type,
154+
description: param.description || ''
155+
};
156+
if (param.required) required.push(param.name);
157+
});
158+
159+
if (requestBody) {
160+
const schema = requestBody.content['application/json'].schema;
161+
Object.assign(properties, schema.properties);
162+
if (schema.required) required.push(...schema.required);
163+
}
164+
165+
tools.push({
166+
type: 'function',
167+
function: {
168+
name: endpoint.operationId || `${method}_${path.replace(/\//g, '_').replace(/[{}]/g, '')}`,
169+
description: endpoint.summary || endpoint.description || '',
170+
parameters: {
171+
type: 'object',
172+
properties,
173+
required
174+
}
175+
}
176+
});
177+
}
178+
}
179+
180+
return tools;
181+
}

0 commit comments

Comments
 (0)