Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "AI Tools" Plugin #8365

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2faba2e
Initial Commit
Jermolene Jul 9, 2024
3b07607
Fix Llamafile compatibility
Jermolene Jul 11, 2024
cb9deaa
Update docs
Jermolene Jul 11, 2024
e00c761
Tweak default prompt
Jermolene Jul 11, 2024
d652f82
Basic support for importing ChatGPT archives
Jermolene Jul 14, 2024
a921034
Improved spinner colours
Jermolene Jul 14, 2024
a1782b1
Palette fixes
Jermolene Jul 16, 2024
95f3e22
Merge branch 'master' into feat-ai-tools
Jermolene Jul 16, 2024
d6f3058
Merge branch 'master' into feat-ai-tools
Jermolene Jul 18, 2024
837374b
Don't try to parse plugins as conversations
Jermolene Jul 18, 2024
a2cff69
Update docs
Jermolene Jul 18, 2024
d569583
Docs tweaks
Jermolene Jul 18, 2024
638bd78
Formatting typo
Jermolene Jul 18, 2024
0e59553
Give conversation tiddlers a dummy text field
Jermolene Jul 19, 2024
dbb7e1c
Merge branch 'master' into feat-ai-tools
Jermolene Jul 21, 2024
0037935
Revert "Give conversation tiddlers a dummy text field"
Jermolene Jul 21, 2024
3bdd449
Don't hardcode the API route
Jermolene Jul 21, 2024
fb641d3
Fix an annoying little bug that prevents importvariables being used i…
Jermolene Jul 21, 2024
370ff30
Refactor completion servers so that they handle their own response
Jermolene Jul 21, 2024
4a79af9
Revise default system prompt
Jermolene Jul 21, 2024
80fdaae
Llamafile use native /completion API endpoint
Jermolene Jul 21, 2024
d39a3d6
Merge branch 'master' into feat-ai-tools
Jermolene Jul 24, 2024
cd58622
Clarify that the Llamafile prompt is for Llava models
Jermolene Jul 24, 2024
28d262e
Merge branch 'master' into feat-ai-tools
Jermolene Jul 27, 2024
a32a1a3
Merge branch 'master' into feat-ai-tools
Jermolene Jul 28, 2024
58f96e7
Add documentation for the conversation format
Jermolene Jul 28, 2024
ea595df
Refactor procedures and functions to be global for reuse
Jermolene Jul 28, 2024
422df10
Fix ChatGPT import
Jermolene Aug 5, 2024
a1a6b6f
Merge branch 'master' into feat-ai-tools
Jermolene Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/modules/startup/rootwidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ exports.startup = function() {
basicAuthUsername: params["basic-auth-username"],
basicAuthUsernameFromStore: params["basic-auth-username-from-store"],
basicAuthPassword: params["basic-auth-password"],
basicAuthPasswordFromStore: params["basic-auth-password-from-store"]
basicAuthPasswordFromStore: params["basic-auth-password-from-store"],
bearerAuthTokenFromStore: params["bearer-auth-token-from-store"]
});
});
$tw.rootWidget.addEventListener("tm-http-cancel-all-requests",function(event) {
Expand Down
5 changes: 5 additions & 0 deletions core/modules/utils/dom/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ basicAuthUsername: plain username for basic authentication
basicAuthUsernameFromStore: name of password store entry containing username
basicAuthPassword: plain password for basic authentication
basicAuthPasswordFromStore: name of password store entry containing password
bearerAuthToken: plain text token for bearer authentication
bearerAuthTokenFromStore: name of password store entry contain bear authorization token
*/
function HttpClientRequest(options) {
var self = this;
Expand Down Expand Up @@ -135,8 +137,11 @@ function HttpClientRequest(options) {
});
this.basicAuthUsername = options.basicAuthUsername || (options.basicAuthUsernameFromStore && $tw.utils.getPassword(options.basicAuthUsernameFromStore)) || "";
this.basicAuthPassword = options.basicAuthPassword || (options.basicAuthPasswordFromStore && $tw.utils.getPassword(options.basicAuthPasswordFromStore)) || "";
this.bearerAuthToken = options.bearerAuthToken || (options.bearerAuthTokenFromStore && $tw.utils.getPassword(options.bearerAuthTokenFromStore)) || "";
if(this.basicAuthUsername && this.basicAuthPassword) {
this.requestHeaders.Authorization = "Basic " + $tw.utils.base64Encode(this.basicAuthUsername + ":" + this.basicAuthPassword);
} else if(this.bearerAuthToken) {
this.requestHeaders.Authorization = "Bearer " + this.bearerAuthToken;
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/action-createtiddler.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ CreateTiddlerWidget.prototype.invokeAction = function(triggeringWidget,event) {
}
this.setVariable("createTiddler-title",title);
this.setVariable("createTiddler-draftTitle",draftTitle);
this.refreshChildren();
this.refreshChildren([]);
return true; // Action was invoked
};

Expand Down
1 change: 1 addition & 0 deletions core/modules/widgets/importvariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ ImportVariablesWidget.prototype.execute = function(tiddlerList) {
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
ImportVariablesWidget.prototype.refresh = function(changedTiddlers) {
changedTiddlers = changedTiddlers || {};
// Recompute our attributes and the filter list
var changedAttributes = this.computeAttributes(),
tiddlerList = this.wiki.filterTiddlers(this.getAttribute("filter"),this);
Expand Down
5 changes: 1 addition & 4 deletions editions/prerelease/tiddlers/system/DefaultTiddlers.tid
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ created: 20131127215321439
modified: 20140912135951542
title: $:/DefaultTiddlers

[[TiddlyWiki Pre-release]]
HelloThere
GettingStarted
Community
$:/plugins/tiddlywiki/ai-tools
4 changes: 3 additions & 1 deletion editions/prerelease/tiddlywiki.info
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"tiddlywiki/jszip",
"tiddlywiki/confetti",
"tiddlywiki/dynannotate",
"tiddlywiki/tour"
"tiddlywiki/tour",
"tiddlywiki/markdown",
"tiddlywiki/ai-tools"
],
"themes": [
"tiddlywiki/vanilla",
Expand Down
4 changes: 1 addition & 3 deletions editions/tw5.com/tiddlers/system/DefaultTiddlers.tid
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@ modified: 20140912135951542
title: $:/DefaultTiddlers
type: text/vnd.tiddlywiki

HelloThere
GettingStarted
Community
$:/plugins/tiddlywiki/ai-tools
2 changes: 1 addition & 1 deletion editions/tw5.com/tiddlers/system/SiteTitle.tid
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ modified: 20131211131023829
title: $:/SiteTitle
type: text/vnd.tiddlywiki

TiddlyWiki @@font-size:small; v<<version>>@@
TiddlyWiki AI Tools Plugin
4 changes: 3 additions & 1 deletion editions/tw5.com/tiddlywiki.info
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"tiddlywiki/menubar",
"tiddlywiki/confetti",
"tiddlywiki/dynannotate",
"tiddlywiki/tour"
"tiddlywiki/tour",
"tiddlywiki/qrcode",
"tiddlywiki/ai-tools"
],
"themes": [
"tiddlywiki/vanilla",
Expand Down
53 changes: 53 additions & 0 deletions plugins/tiddlywiki/ai-tools/docs.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
title: $:/plugins/tiddlywiki/ai-tools/docs

!! Setting Up

See the ''settings'' tab for set up instructions.

!! Live AI Conversations in ~TiddlyWiki

# Click the {{||$:/plugins/tiddlywiki/ai-tools/page-menu}} icon in the sidebar to open a new conversation
# Choose the server from the dropdown:
#* ''Locally running Llamafile server''
#* ''~OpenAI Service'' (requires API key to be specified in ''settings'')
# Type a prompt for the LLM in the text box
#* If using ~OpenAI it is possible to attach a single image to a prompt
# Click "Send" and wait for the output of the LLM

!! Import ~ChatGPT Conversation Archives

# [[Follow the instructions|https://help.openai.com/en/articles/7260999-how-do-i-export-my-chatgpt-history-and-data]] to request an export of your ~ChatGPT data
# You will receive a link to download your data as a ZIP file
# Download and unzip the file
# Locate the file `conversations.json` within the archive and import it into your TiddlyWiki
# Visit the ''tools'' tab and locate your `conversations.json` tiddler
# Click the associated ''import'' button
# See the imported conversations listed in the ''tools'' tab
# The imported tiddler `conversations.json` is no longer required and can be deleted

!! Conversation Format

This plugin defines a simple schema for representing conversations with an LLM.

In a nutshell, tiddlers tagged <<tag $:/tags/AI/Conversation>> define conversations. The individual messages are tiddlers that are tagged with the title of the conversation tiddler.

Currently, the ordering of the messages is determined by the value of their "created" field. The ordering defined by the tag mechanism is ignored. It is intended to change this behaviour so that the ordering of messages is defined by the tag mechanism.

The fields with defined meanings for conversation tiddlers are:

|!Field |!Description |
|''system-prompt'' |Defines the system prompt for the conversation |
|''tags'' |Must include <<tag $:/tags/AI/Conversation>> |
|''text'' |Optional description or notes displayed at the top of the conversation |
|''current-response-image'' |Optional title of an image tiddler to be attached to the current user response |
|''current-response-text'' |Text of the current user response before it is sent |

The fields with defined meanings for conversation tiddlers are:

|!Field |!Description |
|''created'' |Creation date of the message (currently used for ordering) |
|''image'' |Optional image associated with this message |
|''role'' |Possible values include ''user'' and ''assistant'' |
|''tags'' |Must include the title of the parent conversation |
|''type'' |Typically ''text/markdown'' |

219 changes: 219 additions & 0 deletions plugins/tiddlywiki/ai-tools/globals.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
title: $:/plugins/tiddlywiki/ai-tools/globals
tags: $:/tags/Global

\function ai-tools-default-llm-completion-server()
[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]first[]]
\end

<!--
Action procedure to retrieve an LLM completion, given the following parameters:
conversationTitle - Title of the tiddler containing the conversation
resultTitlePrefix - Prefix of the tiddler to be used for saving the result. If the tiddler already exists then a number will be added repeatedly until the resulting title is unique
resultTags - Tags to be applied to the result tiddler
ai-tools-status-title - Optional title of a tiddler to which the status of the request will be bound: "pending", "complete", "error"
completionServer - Optional URL of server
-->
\procedure ai-tools-get-llm-completion(conversationTitle,resultTitlePrefix,resultTags,ai-tools-status-title,completionServer)
<$let
completionServer={{{ [<completionServer>!is[blank]else<ai-tools-default-llm-completion-server>] }}}
>
<$importvariables filter="[<completionServer>]">
<$wikify name="json" text=<<json-prompt>>>
<$action-log message="ai-tools-get-llm-completion"/>
<$action-log/>
<$action-sendmessage
$message="tm-http-request"
url={{{ [<completionServer>get[url]] }}}
body=<<json>>
header-content-type="application/json"
bearer-auth-token-from-store="openai-secret-key"
method="POST"
oncompletion=<<completion-callback>>
bind-status=<<ai-tools-status-title>>
var-resultTitlePrefix=<<resultTitlePrefix>>
var-resultTags=<<resultTags>>
/>
</$wikify>
</$importvariables>
</$let>
\end ai-tools-get-llm-completion

<!--
-->
\function ai-tools-status-title()
[<currentTiddler>addprefix[$:/temp/ai-tools/status/]]
\end ai-tools-status-title

<!--
Procedure to display a message from an AI conversation. Current tiddler is the conversation tiddler
-->
\procedure ai-tools-message(tiddler,field,role,makeLink:"yes")
<$qualify
name="state"
title={{{ [[$:/state/ai-tools-message-state/]addsuffix<tiddler>] }}}
>
<$let
editStateTiddler={{{ [<state>addsuffix[-edit-state]] }}}
editState={{{ [<editStateTiddler>get[text]else[view]] }}}
>
<div class={{{ ai-tools-message [<role>addprefix[ai-tools-message-role-]] +[join[ ]] }}}>
<div class="ai-tools-message-toolbar">
<div class="ai-tools-message-toolbar-left">
<$genesis $type={{{ [<makeLink>match[yes]then[$link]else[span]] }}} to=<<tiddler>>>
<$text text=<<role>>/>
</$genesis>
</div>
<div class="ai-tools-message-toolbar-left">
<%if [<editState>!match[edit]] %>
<$button class="ai-tools-message-toolbar-button">
<$action-setfield $tiddler=<<editStateTiddler>> text="edit"/>
edit
</$button>
<%endif%>
<%if [<editState>!match[view]] %>
<$button class="ai-tools-message-toolbar-button">
<$action-setfield $tiddler=<<editStateTiddler>> text="view"/>
view
</$button>
<%endif%>
<$button class="ai-tools-message-toolbar-button">
<$action-sendmessage $message="tm-copy-to-clipboard" $param={{{ [<tiddler>get<field>else[]] }}}/>
copy
</$button>
<$button class="ai-tools-message-toolbar-button">
<$action-deletetiddler $tiddler=<<tiddler>>/>
delete
</$button>
</div>
</div>
<div class="ai-tools-message-body">
<%if [<editState>match[view]] %>
<$transclude $tiddler=<<tiddler>> $field=<<field>> $mode="block"/>
<%else%>
<$edit-text tiddler=<<tiddler>> field=<<field>> tag="textarea" class="tc-edit-texteditor"/>
<%endif%>
<%if [<tiddler>get[image]else[]!match[]] %>
<$image source={{{ [<tiddler>get[image]] }}}/>
<%endif%>
</div>
</div>
</$let>
</$qualify>
\end ai-tools-message

<!--
Action procedure to get the next response from the LLM
-->
\procedure ai-tools-action-get-response()
<$let
resultTitlePrefix={{{ [<currentTiddler>addsuffix[ - Prompt]] }}}
resultTags={{{ [<currentTiddler>format:titlelist[]] }}}
>
<$action-createtiddler
$basetitle=<<resultTitlePrefix>>
tags=<<resultTags>>
type="text/markdown"
role="user"
text={{!!current-response-text}}
image={{!!current-response-image}}
>
<$action-deletefield $tiddler=<<currentTiddler>> $field="current-response-text"/>
<$action-deletefield $tiddler=<<currentTiddler>> $field="current-response-image"/>
<$transclude
$variable="ai-tools-get-llm-completion"
conversationTitle=<<currentTiddler>>
completionServer={{!!completion-server}}
resultTitlePrefix=<<resultTitlePrefix>>
resultTags=<<resultTags>>
ai-tools-status-title=<<ai-tools-status-title>>
/>
</$action-createtiddler>
</$let>
\end ai-tools-action-get-response

\procedure ai-tools-conversation(conversationTitle)
<$let currentTiddler=<<conversationTitle>>>

Server: <$select tiddler=<<currentTiddler>> field="completion-server" default=<<ai-tools-default-llm-completion-server>>>
<$list filter="[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]]">
<option value=<<currentTiddler>>><$view field='caption'/></option>
</$list>
</$select>

<div class="ai-conversation">
<$transclude
$variable="ai-tools-message"
tiddler=<<currentTiddler>>
field="system-prompt"
role="system"
makeLink="no"
/>
<$list filter="[all[shadows+tiddlers]tag<currentTiddler>!is[draft]sort[created]]" variable="message" storyview="pop">
<$transclude
$variable="ai-tools-message"
tiddler=<<message>>
field="text"
role={{{ [<message>get[role]] }}}
/>
</$list>
<%if [<ai-tools-status-title>get[text]else[complete]match[pending]] %>
<div class="ai-request-status">
<div class="ai-request-spinner"></div>
</div>
<%endif%>
<div class="ai-user-prompt">
<div class="ai-user-prompt-text">
<$edit-text tiddler=<<currentTiddler>> field="current-response-text" tag="textarea" class="tc-edit-texteditor"/>
<$button
class="ai-user-prompt-send"
actions=<<ai-tools-action-get-response>>
disabled={{{ [<ai-tools-status-title>get[text]else[complete]match[pending]then[yes]] [<currentTiddler>get[current-response-text]else[]match[]then[yes]] ~[[no]] }}}
>
Send
</$button>
</div>
<div class="ai-user-prompt-image">
<div class="tc-drop-down-wrapper">
<$let state=<<qualify "$:/state/ai-user-prompt-image-dropdown-state/">>>
<$button popup=<<state>> class="tc-btn-invisible tc-btn-dropdown">Choose an image {{$:/core/images/down-arrow}}</$button>
<$link to={{!!current-response-image}}>
<$text text={{!!current-response-image}}/>
</$link>
<$reveal state=<<state>> type="popup" position="belowleft" text="" default="" class="tc-popup-keep">
<div class="tc-drop-down" style="text-align:center;">
<$transclude
$variable="image-picker"
filter="[all[shadows+tiddlers]is[image]is[binary]!has[_canonical_uri]] -[type[application/pdf]] +[!has[draft.of]sort[title]]"
actions="""
<$action-setfield
$tiddler=<<currentTiddler>>
current-response-image=<<imageTitle>>
/>
<$action-deletetiddler $tiddler=<<state>>/>
"""
/>
</div>
</$reveal>
</$let>
<$image source={{!!current-response-image}}/>
</div>
</div>
</div>
</div>
</$let>
\end ai-tools-conversation

\procedure ai-tools-new-conversation()
<$action-createtiddler
$basetitle="AI Conversation"
tags="$:/tags/AI/Conversation"
system-prompt="Transcript of a never ending dialog, where the User interacts with an Assistant. The Assistant is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision."
current-response-text="Please list the 10 most important mechanical inventions of the Twentieth Century"
>
<$action-navigate $to=<<createTiddler-title>>/>
</$action-createtiddler>
\end ai-tools-new-conversation

\procedure ai-tools-import-conversations()
<$action-navigate $to="$:/plugins/tiddlywiki/ai-tools/tools"/>
\end ai-tools-import-conversations
Loading
Loading