Skip to content

Commit

Permalink
Merge pull request #95 from zamm-dev/edit-api-calls
Browse files Browse the repository at this point in the history
Add ability to edit API calls
  • Loading branch information
amosjyng authored Jun 11, 2024
2 parents 6312c4f + a9d2a66 commit ab6fb05
Show file tree
Hide file tree
Showing 45 changed files with 1,504 additions and 77 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src-svelte/src/lib/ApiCallReference.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
import ApiCallReferenceLink from "./ApiCallReferenceLink.svelte";
import type { LlmCallReference } from "./bindings";
export let apiCall: LlmCallReference;
export let selfContained = false;
</script>

{#if selfContained}
<div class="ellipsis-container">
<ApiCallReferenceLink {apiCall} {...$$restProps} />
</div>
{:else}
<ApiCallReferenceLink {apiCall} {...$$restProps} />
{/if}

<style>
.ellipsis-container {
display: flex;
width: 100%;
}
</style>
27 changes: 27 additions & 0 deletions src-svelte/src/lib/ApiCallReferenceLink.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import type { LlmCallReference } from "$lib/bindings";
export let apiCall: LlmCallReference;
export let nolink = false;
</script>

{#if nolink}
<span>{apiCall.snippet}</span>
{:else}
<a href="/api-calls/{apiCall.id}">{apiCall.snippet}</a>
{/if}

<style>
span,
a {
min-width: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
span {
font-style: italic;
}
</style>
20 changes: 19 additions & 1 deletion src-svelte/src/lib/__mocks__/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import {
} from "$lib/preferences";
import { systemInfo } from "$lib/system-info";
import { conversation } from "../../routes/chat/Chat.svelte";
import type { SystemInfo, ChatMessage } from "$lib/bindings";
import {
canonicalRef,
getDefaultApiCall,
prompt,
} from "../../routes/api-calls/new/ApiCallEditor.svelte";
import type {
SystemInfo,
ChatMessage,
Prompt,
LlmCallReference,
} from "$lib/bindings";
import { firstAppLoad, firstPageLoad } from "$lib/firstPageLoad";

interface Preferences {
Expand All @@ -15,9 +25,15 @@ interface Preferences {
animationSpeed?: number;
}

interface ApiCallEditing {
canonicalRef: LlmCallReference;
prompt: Prompt;
}

interface Stores {
systemInfo?: SystemInfo;
conversation?: ChatMessage[];
apiCallEditing?: ApiCallEditing;
}

interface StoreArgs {
Expand Down Expand Up @@ -55,6 +71,8 @@ const SvelteStoresDecorator: Decorator = (

systemInfo.set(stores?.systemInfo);
conversation.set(stores?.conversation || []);
canonicalRef.set(stores?.apiCallEditing?.canonicalRef);
prompt.set(stores?.apiCallEditing?.prompt || getDefaultApiCall());

return story(args, context);
};
Expand Down
48 changes: 28 additions & 20 deletions src-svelte/src/lib/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,26 @@ export function getApiCalls(offset: number) {
return invoke()<LightweightLlmCall[]>("get_api_calls", { offset });
}

export type TokenMetadata = {
prompt: number | null;
response: number | null;
total: number | null;
};
export type Request = { prompt: Prompt; temperature: number };
export type ChatMessage =
| { role: "System"; text: string }
| { role: "Human"; text: string }
| { role: "AI"; text: string };
export type Llm = { name: string; requested: string; provider: Service };
export type VariantMetadata = {
canonical?: LlmCallReference | null;
variants?: LlmCallReference[];
sibling_variants?: LlmCallReference[];
};
export type Prompt = { type: "Chat" } & ChatPrompt;
export type Request = { prompt: Prompt; temperature: number };
export type Service = "OpenAI";
export type Response = { completion: ChatMessage };
export type ConversationMetadata = {
previous_call?: LlmCallReference | null;
next_calls?: LlmCallReference[];
};
export type Preferences = {
version?: string | null;
animations_on?: boolean | null;
background_animation?: boolean | null;
animation_speed?: number | null;
Expand All @@ -79,6 +85,16 @@ export type LightweightLlmCall = {
timestamp: string;
response_message: ChatMessage;
};
export type LlmCall = {
id: EntityId;
timestamp: string;
llm: Llm;
request: Request;
response: Response;
tokens: TokenMetadata;
conversation?: ConversationMetadata;
variation?: VariantMetadata;
};
export type ApiKeys = { openai: string | null };
export type OS = "Mac" | "Linux" | "Windows";
export type EntityId = string;
Expand All @@ -90,26 +106,18 @@ export type SystemInfo = {
shell_init_file: string | null;
};
export type ChatPrompt = { messages: ChatMessage[] };
export type TokenMetadata = {
prompt: number | null;
response: number | null;
total: number | null;
export type Response = { completion: ChatMessage };
export type ConversationMetadata = {
previous_call?: LlmCallReference | null;
next_calls?: LlmCallReference[];
};
export type LlmCallReference = { id: EntityId; snippet: string };
export type ChatArgs = {
provider: Service;
llm: string;
temperature?: number | null;
prompt: ChatMessage[];
previous_call_id?: string | null;
canonical_id?: string | null;
};
export type LlmCall = {
id: EntityId;
timestamp: string;
llm: Llm;
request: Request;
response: Response;
tokens: TokenMetadata;
conversation?: ConversationMetadata;
};
export type LlmCallReference = { id: EntityId; snippet: string };
export type Sound = "Switch" | "Whoosh";
14 changes: 13 additions & 1 deletion src-svelte/src/lib/controls/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createEventDispatcher } from "svelte";
export let unwrapped = false;
export let leftEnd = false;
export let rightEnd = false;
export let ariaLabel: string | undefined = undefined;
const dispatchClickEvent = createEventDispatcher();
Expand All @@ -14,6 +15,7 @@
{#if unwrapped}
<button
class="cut-corners inner"
class:left-end={leftEnd}
class:right-end={rightEnd}
type="submit"
aria-label={ariaLabel}
Expand All @@ -24,12 +26,17 @@
{:else}
<button
class="cut-corners outer"
class:left-end={leftEnd}
class:right-end={rightEnd}
type="submit"
aria-label={ariaLabel}
on:click={handleClick}
>
<div class="cut-corners inner" class:right-end={rightEnd}>
<div
class="cut-corners inner"
class:left-end={leftEnd}
class:right-end={rightEnd}
>
<slot />
</div>
</button>
Expand All @@ -46,6 +53,11 @@
transition: calc(0.5 * var(--standard-duration)) ease-out;
}
.outer.left-end,
.inner.left-end {
--cut-bottom-right: 0.01rem;
}
.outer.right-end,
.inner.right-end {
--cut-top-left: 0.01rem;
Expand Down
41 changes: 40 additions & 1 deletion src-svelte/src/routes/api-calls/[slug]/Actions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@
import InfoBox from "$lib/InfoBox.svelte";
import { type LlmCall } from "$lib/bindings";
import { lastMessageId, conversation } from "../../chat/Chat.svelte";
import { canonicalRef, prompt } from "../new/ApiCallEditor.svelte";
import { goto } from "$app/navigation";
import { snackbarError } from "$lib/snackbar/Snackbar.svelte";
import Button from "$lib/controls/Button.svelte";
export let apiCall: LlmCall | undefined = undefined;
function editApiCall() {
if (!apiCall) {
snackbarError("API call not yet loaded");
return;
}
canonicalRef.set({
id: apiCall.id,
snippet: apiCall.response.completion.text,
});
prompt.set(apiCall.request.prompt);
goto("/api-calls/new/");
}
function restoreConversation() {
if (!apiCall) {
snackbarError("API call not yet loaded");
Expand All @@ -27,7 +43,12 @@

<InfoBox title="Actions" childNumber={1}>
<div class="action-buttons">
<Button on:click={restoreConversation}>Restore conversation</Button>
<div class="button-container cut-corners outer">
<Button unwrapped leftEnd on:click={editApiCall}>Edit API call</Button>
<Button unwrapped rightEnd on:click={restoreConversation}
>Restore conversation</Button
>
</div>
</div>
</InfoBox>

Expand All @@ -36,4 +57,22 @@
width: fit-content;
margin: 0 auto;
}
.button-container {
display: flex;
}
.button-container :global(.left-end) {
--cut-top-left: 8px;
}
.button-container :global(.right-end) {
--cut-bottom-right: 8px;
}
@media (max-width: 35rem) {
.button-container {
flex-direction: column;
}
}
</style>
73 changes: 65 additions & 8 deletions src-svelte/src/routes/api-calls/[slug]/ApiCall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import {
conversation,
lastMessageId,
} from "../../chat/Chat.svelte";
import {
canonicalRef,
prompt,
getDefaultApiCall,
resetNewApiCall,
} from "../new/ApiCallEditor.svelte";
import { EDIT_CANONICAL_REF, EDIT_PROMPT } from "../new/test.data";
import { TauriInvokePlayback } from "$lib/sample-call-testing";
import ApiCall from "./ApiCall.svelte";
import { get } from "svelte/store";
Expand All @@ -25,11 +32,17 @@ describe("Individual API call", () => {
(...args: (string | Record<string, string>)[]) =>
playback.mockCall(...args),
);

mockStores.page.set({
url: new URL("http://localhost/"),
params: {},
});
resetConversation();
resetNewApiCall();
});

afterEach(() => {
vi.unstubAllGlobals();
resetConversation();
});

function expectRowValue(key: string, expectedValue: string) {
Expand Down Expand Up @@ -57,17 +70,39 @@ describe("Individual API call", () => {
// check that human message is displayed
expect(screen.getByText("Hello, does this work?")).toBeInTheDocument();
// check that AI message is displayed
expect(
screen.getByText(
"Sure, here's a joke for you: " +
"Why don't scientists trust atoms? " +
"Because they make up everything!",
),
).toBeInTheDocument();
const responseSection = await screen.findByLabelText("Response");
const response = responseSection.querySelector("pre");
expect(response).toHaveTextContent(
"Sure, here's a joke for you: Why don't scientists trust atoms? " +
"Because they make up everything!",
);

// check that metadata is displayed
expectRowValue("LLM", "gpt-4 → gpt-4-0613");
expectRowValue("Temperature", "1.00");
expectRowValue("Tokens", "57 prompt + 22 response = 79 total tokens");

// check that links are displayed
const conversationSection = await screen.findByLabelText("Conversation");
const conversationLinks = Array.from(
conversationSection.querySelectorAll("a"),
).map((a) => a.href);
expect(conversationLinks).toEqual([
// previous
"http://localhost:3000/api-calls/d5ad1e49-f57f-4481-84fb-4d70ba8a7a74",
// next
"http://localhost:3000/api-calls/0e6bcadf-2b41-43d9-b4cf-81008d4f4771",
"http://localhost:3000/api-calls/63b5c02e-b864-4efe-a286-fbef48b152ef",
]);

const variantSection = await screen.findByLabelText("Variants");
const variantLinks = Array.from(variantSection.querySelectorAll("a")).map(
(a) => a.href,
);
expect(variantLinks).toEqual([
"http://localhost:3000/api-calls/f39a5017-89d4-45ec-bcbb-25c2bd43cfc1",
"http://localhost:3000/api-calls/7a35a4cf-f3d9-4388-bca8-2fe6e78c9648",
]);
});

test("can restore chat conversation", async () => {
Expand Down Expand Up @@ -121,4 +156,26 @@ describe("Individual API call", () => {
expect(get(lastMessageId)).toEqual("c13c1e67-2de3-48de-a34c-a32079c03316");
expect(get(mockStores.page).url.pathname).toEqual("/chat");
});

test("can edit API call", async () => {
playback.addSamples(
"../src-tauri/api/sample-calls/get_api_call-continue-conversation.yaml",
);
render(ApiCall, { id: "c13c1e67-2de3-48de-a34c-a32079c03316" });
expect(tauriInvokeMock).toHaveReturnedTimes(1);
await waitFor(() => {
screen.getByText("Hello, does this work?");
});
expect(get(canonicalRef)).toBeUndefined();
expect(get(prompt)).toEqual(getDefaultApiCall());
expect(get(mockStores.page).url.pathname).toEqual("/");

const editButton = await waitFor(() => screen.getByText("Edit API call"));
userEvent.click(editButton);
await waitFor(() => {
expect(get(prompt)).toEqual(EDIT_PROMPT);
});
expect(get(canonicalRef)).toEqual(EDIT_CANONICAL_REF);
expect(get(mockStores.page).url.pathname).toEqual("/api-calls/new/");
});
});
Loading

0 comments on commit ab6fb05

Please sign in to comment.