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

llm web command - launches a web server #4

Open
simonw opened this issue Apr 2, 2023 · 9 comments
Open

llm web command - launches a web server #4

simonw opened this issue Apr 2, 2023 · 9 comments
Labels
enhancement New feature or request

Comments

@simonw
Copy link
Owner

simonw commented Apr 2, 2023

A command that launches a web server to allow you to both browse your logs and interact with APIs through your browser.

@simonw simonw added the enhancement New feature or request label Apr 2, 2023
@simonw
Copy link
Owner Author

simonw commented Apr 2, 2023

I'm tempted to add Datasette as a dependency for this.

@simonw
Copy link
Owner Author

simonw commented Apr 2, 2023

Maybe this becomes a two-way thing: it could be a Datasette plugin (which adds datasette llm as a command, and implements the full UI in Datasette) but it could also be a thing where if you install llm and run llm web you get a Datasette interface running that plugin.

@simonw
Copy link
Owner Author

simonw commented Apr 2, 2023

Prototype:

import asyncio
from datasette import hookimpl, Response
import openai



CHAT = """
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Client</title>
</head>
<body>
    <h1>WebSocket Client</h1>
    <textarea id="message" rows="4" cols="50"></textarea><br>
    <button onclick="sendMessage()">Send Message</button>
    <div id="log" style="margin-top: 1em; white-space: pre-wrap;"></div>

    <script>
        const ws = new WebSocket(`ws://${location.host}/ws`);

        ws.onmessage = function(event) {
            console.log(event);
            const log = document.getElementById('log');
            log.textContent += event.data;
        };

        function sendMessage() {
            const message = document.getElementById('message').value;
            console.log({message, ws});
            ws.send(message);
        }
    </script>
</body>
</html>
""".strip()


async def websocket_application(scope, receive, send):
    from .cli import get_openai_api_key
    openai.api_key = get_openai_api_key()
    if scope["type"] != "websocket":
        return Response.text("ws only", status=400)
    while True:
        event = await receive()
        if event["type"] == "websocket.connect":
            await send({"type": "websocket.accept"})
        elif event["type"] == "websocket.receive":
            message = event["text"]

            async for chunk in await openai.ChatCompletion.acreate(
                model="gpt-3.5-turbo",
                messages=[{
                    "role": "user",
                    "content": message,
                }],
                stream=True,
            ):
                content = chunk["choices"][0].get("delta", {}).get("content")
                if content is not None:
                    await send({"type": "websocket.send", "text": content})

        elif event["type"] == "websocket.disconnect":
            break


def chat():
    return Response.html(CHAT)


@hookimpl
def register_routes():
    return [
        (r"^/ws$", websocket_application),
        (r"^/chat$", chat),
    ]

I put that in llm/plugin.py and then put this in setup.py:

    entry_points={
        "datasette": ["llm = llm.plugin"],
        "console_scripts": ["llm=llm.cli:cli"],
    },

And this in cli.py:

@cli.command()
def web():
    from datasette.app import Datasette
    import uvicorn

    path = get_log_db_path()
    if not os.path.exists(path):
        sqlite_utils.Database(path).vacuum()
    ds = Datasette(
        [path],
        metadata={
            "databases": {
                "log": {
                    "tables": {
                        "log": {
                            "sort_desc": "rowid",
                        }
                    }
                }
            }
        },
    )
    uvicorn.run(ds.app(), host="0.0.0.0", port=8302)

@simonw
Copy link
Owner Author

simonw commented Jul 4, 2023

Mucked around with HTML and CSS a bit and got to this prototype:

<div class="chat-container">
  <div class="chat-bubble one">
    <div>
        <p>Hello, how are you?</p>
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person A Avatar">
  </div>
  <div class="chat-bubble two">
    <div>
        <p>I'm good, thanks! And you?</p>
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person B Avatar">
  </div>
  <div class="chat-bubble one">
    <div>
        <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet</p><p>aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person A Avatar">
  </div>
  <div class="chat-bubble two">
    <div id="animatedText">
        <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet</p><p>aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person B Avatar">
  </div>
</div>
<style>
.chat-container {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    font-family: Helvetica, sans-serif;
    line-height: 1.35;
    color: rgba(0, 0, 0, 0.8);
    max-width: 600px;

}

.chat-bubble {
    border-radius: 10px;
    padding: 10px;
    margin: 10px;
    width: 85%;
    border: 1px solid #ccc;
    background-color: #e6e5ff;

    display: flex;
    align-items: start;
}
.chat-bubble.one {
    border-color: #b9b7f2;
}
.chat-bubble.two {
    /* darker darker green */
    border-color: #98d798;
}

.chat-bubble.one img.avatar {
    order: -1;
    margin-right: 10px;
}

.chat-bubble.two {
    background-color: #ccffcc;
    align-self: flex-end;
    justify-content: space-between;
}

.chat-bubble.two img.avatar {
    order: 1;
    margin-left: 10px;
}

.chat-bubble p {
    margin-top: 0;
}
.chat-bubble p:last-of-type {
    margin-bottom: 0;
}

</style>

<script>
var text = "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?";
var words = text.split(" ");
var container = document.getElementById("animatedText");
container.innerHTML = "";

function addWord(index) {
if (index < words.length) {
    container.innerHTML += words[index] + " ";
    setTimeout(function() {
    addWord(index + 1);
    }, 50);
}
}

addWord(0);
</script>

prototype

@simonw
Copy link
Owner Author

simonw commented Jul 4, 2023

Added a submit form:

<div class="chat-container">
  <div class="chat-bubble one">
    <div>
        <p>Hello, how are you?</p>
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person A Avatar">
  </div>
  <div class="chat-bubble two">
    <div>
        <p>I'm good, thanks! And you?</p>
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person B Avatar">
  </div>
  <div class="chat-bubble one">
    <div>
        <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet</p><p>aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person A Avatar">
  </div>
  <div class="chat-bubble two">
    <div id="animatedText">
        <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet</p><p>aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person B Avatar">
  </div>
  <div class="chat-bubble one">
    <div class="contains-textarea">
        <form action="">
        <textarea>Type here</textarea>
        <p class="submit"><input type="submit" value="Send"></p>
        </form>
    </div>
    <img class="avatar" src="https://placekitten.com/40/40" alt="Person A Avatar">
  </div>

</div>
<style>
.chat-container form {
    margin: 0;
}
.contains-textarea {
    /* flex box should take all available width */
    flex: 1;
}
p.submit {
    text-align: right;
    padding-top: 5px;
}
p.submit input {
    border: 2px solid #7572db;
    padding: 3 10px;
    background-color: #b9b7f2;
}
textarea {
    width: 100%;
    padding: 5px;
    min-height: 60px;   
}
.chat-container {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    font-family: Helvetica, sans-serif;
    line-height: 1.35;
    color: rgba(0, 0, 0, 0.8);
    max-width: 600px;

}

.chat-bubble {
    border-radius: 10px;
    padding: 10px;
    margin: 10px;
    width: 85%;
    border: 1px solid #ccc;
    background-color: #e6e5ff;

    display: flex;
    align-items: start;
}
.chat-bubble.one {
    border-color: #b9b7f2;
}
.chat-bubble.two {
    /* darker darker green */
    border-color: #98d798;
}

.chat-bubble.one img.avatar {
    order: -1;
    margin-right: 10px;
}

.chat-bubble.two {
    background-color: #ccffcc;
    align-self: flex-end;
    justify-content: space-between;
}

.chat-bubble.two img.avatar {
    order: 1;
    margin-left: 10px;
}

.chat-bubble p {
    margin-top: 0;
}
.chat-bubble p:last-of-type {
    margin-bottom: 0;
}

</style>

<script>
var text = "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio similique quos ratione omnis impedit, est mollitia amet aspernatur inventore consectetur, autem dolorum at nemo! Voluptas modi eveniet culpa nobis id?";
var words = text.split(" ");
var container = document.getElementById("animatedText");
container.innerHTML = "";

function addWord(index) {
if (index < words.length) {
    container.innerHTML += words[index] + " ";
    setTimeout(function() {
    addWord(index + 1);
    }, 50);
}
}

addWord(0);
</script>
image

@simonw
Copy link
Owner Author

simonw commented Jul 10, 2023

I built a quick ASGI prototype demonstrating server-sent events here: https://gist.github.com/simonw/d3d4773666b863e628b1a60d5a20294d

simonw added a commit that referenced this issue Jul 14, 2023
@simonw
Copy link
Owner Author

simonw commented Jul 14, 2023

Pushed a prototype to the web branch.

chat

Currently needs a OPENAI_API_KEY environment variable. I still need to port it to using llm directly: https://llm.datasette.io/en/stable/python-api.html

@simonw simonw added this to the 0.6 milestone Jul 15, 2023
@JBX028
Copy link

JBX028 commented Jul 17, 2023

Hi,
First of all, thanks a lot for having created LLM as well as the other CLI tools.
Would be so useful to have something like this in order to interact with LLM with http or websockets requests. I am a nodejs developer and could easily integrate any of the llm engines (since the last version) in my apps without to manage the complex installation of the various llm engines. Any ETA about this feature?

simonw added a commit that referenced this issue Aug 2, 2023
simonw added a commit that referenced this issue Aug 2, 2023
simonw added a commit that referenced this issue Aug 2, 2023
The code for this is pretty messy but it works. Refs #4
simonw added a commit that referenced this issue Aug 2, 2023
simonw added a commit that referenced this issue Aug 2, 2023
@simonw simonw modified the milestones: 0.6, 0.8 Aug 18, 2023
@thiswillbeyourgithub
Copy link

Why not just use gradio? In about 50 lines of code you could have tabs for a chat interface, displaying and interacting with your logs etc.

I've been using it for a while and is terrific

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants