Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit fdf0ff8

Browse files
authored
FEATURE: persistent key-value storage for AI Artifacts (#1417)
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.
1 parent f7e0ea8 commit fdf0ff8

File tree

26 files changed

+1238
-51
lines changed

26 files changed

+1238
-51
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module AiBot
5+
class ArtifactKeyValuesController < ::ApplicationController
6+
requires_plugin DiscourseAi::PLUGIN_NAME
7+
before_action :ensure_logged_in, only: %i[set destroy]
8+
before_action :find_artifact
9+
10+
PER_PAGE_MAX = 100
11+
12+
def index
13+
page = index_params[:page].to_i
14+
page = 1 if page < 1
15+
per_page = index_params[:per_page].to_i
16+
per_page = PER_PAGE_MAX if per_page < 1 || per_page > PER_PAGE_MAX
17+
18+
query = build_index_query
19+
20+
total_count = query.count
21+
key_values =
22+
query
23+
.includes(:user)
24+
.order(:user_id, :key, :created_at)
25+
.offset((page - 1) * per_page)
26+
.limit(per_page + 1)
27+
28+
has_more = key_values.length > per_page
29+
key_values = key_values.first(per_page) if has_more
30+
31+
render json: {
32+
key_values:
33+
ActiveModel::ArraySerializer.new(
34+
key_values,
35+
each_serializer: AiArtifactKeyValueSerializer,
36+
keys_only: params[:keys_only] == "true",
37+
).as_json,
38+
has_more: has_more,
39+
total_count: total_count,
40+
users:
41+
key_values
42+
.map { |kv| kv.user }
43+
.uniq
44+
.map { |u| BasicUserSerializer.new(u, root: nil).as_json },
45+
}
46+
end
47+
48+
def destroy
49+
if params[:key].blank?
50+
render json: { error: "Key parameter is required" }, status: :bad_request
51+
return
52+
end
53+
54+
key_value = @artifact.key_values.find_by(user_id: current_user.id, key: params[:key])
55+
56+
if key_value.nil?
57+
render json: { error: "Key not found" }, status: :not_found
58+
elsif key_value.destroy
59+
head :ok
60+
else
61+
render json: { errors: key_value.errors.full_messages }, status: :unprocessable_entity
62+
end
63+
end
64+
65+
def set
66+
key_value =
67+
@artifact.key_values.find_or_initialize_by(
68+
user: current_user,
69+
key: key_value_params[:key],
70+
)
71+
72+
key_value.assign_attributes(key_value_params.except(:key))
73+
74+
if key_value.save
75+
render json: AiArtifactKeyValueSerializer.new(key_value).as_json
76+
else
77+
render json: { errors: key_value.errors.full_messages }, status: :unprocessable_entity
78+
end
79+
end
80+
81+
private
82+
83+
def key_value_params
84+
params.permit(:key, :value, :public)
85+
end
86+
87+
def index_params
88+
@index_params ||= params.permit(:page, :per_page, :key, :keys_only, :all_users)
89+
end
90+
91+
def build_index_query
92+
query = @artifact.key_values
93+
94+
query =
95+
if current_user&.admin?
96+
query
97+
elsif current_user
98+
query.where("user_id = ? OR public = true", current_user.id)
99+
else
100+
query.where(public: true)
101+
end
102+
103+
query = query.where("key = ?", index_params[:key]) if index_params[:key].present?
104+
105+
if !index_params[:all_users].to_s == "true" && current_user
106+
query = query.where(user_id: current_user.id)
107+
end
108+
109+
query
110+
end
111+
112+
def find_artifact
113+
@artifact = AiArtifact.find_by(id: params[:artifact_id])
114+
raise Discourse::NotFound if !@artifact
115+
raise Discourse::NotFound if !@artifact.public? && guardian.anonymous?
116+
raise Discourse::NotFound if !@artifact.public? && !guardian.can_see?(@artifact.post)
117+
end
118+
end
119+
end
120+
end

0 commit comments

Comments
 (0)