mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-11-10 03:09:02 +00:00
Introduces a persistent, user-scoped key-value storage system for
AI Artifacts, enabling them to be stateful and interactive. This
transforms artifacts from static content into mini-applications that can
save user input, preferences, and other data.
The core components of this feature are:
1. **Model and API**:
- A new `AiArtifactKeyValue` model and corresponding database table to
store data associated with a user and an artifact.
- A new `ArtifactKeyValuesController` provides a RESTful API for
CRUD operations (`index`, `set`, `destroy`) on the key-value data.
- Permissions are enforced: users can only modify their own data but
can view public data from other users.
2. **Secure JavaScript Bridge**:
- A `postMessage` communication bridge is established between the
sandboxed artifact `iframe` and the parent Discourse window.
- A JavaScript API is exposed to the artifact as `window.discourseArtifact`
with async methods: `get(key)`, `set(key, value, options)`,
`delete(key)`, and `index(filter)`.
- The parent window handles these requests, makes authenticated calls to the
new controller, and returns the results to the iframe. This ensures
security by keeping untrusted JS isolated.
3. **AI Tool Integration**:
- The `create_artifact` tool is updated with a `requires_storage`
boolean parameter.
- If an artifact requires storage, its metadata is flagged, and the
system prompt for the code-generating AI is augmented with detailed
documentation for the new storage API.
4. **Configuration**:
- Adds hidden site settings `ai_artifact_kv_value_max_length` and
`ai_artifact_max_keys_per_user_per_artifact` for throttling.
This also includes a minor fix to use `jsonb_set` when updating
artifact metadata, ensuring other metadata fields are preserved.
69 lines
1.8 KiB
Ruby
69 lines
1.8 KiB
Ruby
# frozen_string_literal: true
|
|
module DiscourseAi
|
|
module Personas
|
|
module ArtifactUpdateStrategies
|
|
class InvalidFormatError < StandardError
|
|
end
|
|
class Base
|
|
attr_reader :post, :user, :artifact, :artifact_version, :instructions, :llm, :cancel_manager
|
|
|
|
def initialize(
|
|
llm:,
|
|
post:,
|
|
user:,
|
|
artifact:,
|
|
artifact_version:,
|
|
instructions:,
|
|
cancel_manager: nil
|
|
)
|
|
@llm = llm
|
|
@post = post
|
|
@user = user
|
|
@artifact = artifact
|
|
@artifact_version = artifact_version
|
|
@instructions = instructions
|
|
@cancel_manager = cancel_manager
|
|
end
|
|
|
|
def apply(&progress)
|
|
changes = generate_changes(&progress)
|
|
parsed_changes = parse_changes(changes)
|
|
apply_changes(parsed_changes)
|
|
end
|
|
|
|
def storage_api
|
|
if @artifact.metadata.is_a?(Hash) && @artifact.metadata["requires_storage"]
|
|
DiscourseAi::Personas::Tools::CreateArtifact.storage_api
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def generate_changes(&progress)
|
|
response = +""
|
|
llm.generate(build_prompt, user: user, cancel_manager: cancel_manager) do |partial|
|
|
progress.call(partial) if progress
|
|
response << partial
|
|
end
|
|
response
|
|
end
|
|
|
|
def build_prompt
|
|
# To be implemented by subclasses
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def parse_changes(response)
|
|
# To be implemented by subclasses
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def apply_changes(changes)
|
|
# To be implemented by subclasses
|
|
raise NotImplementedError
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|