Skip to content

Commit

Permalink
wip: implementing chats
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Mar 13, 2024
1 parent 2db5ce5 commit a9b96b5
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 22 deletions.
1 change: 1 addition & 0 deletions ckanext/chatbot/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FLAKE_CHATS = "ckanext:chatbot:chat:{}"
41 changes: 29 additions & 12 deletions ckanext/chatbot/templates/chatbot/talk.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,48 @@

{% import 'macros/form.html' as form %}

{% block secondary %}{% endblock secondary %}

{% block page_header %}{% endblock page_header %}

{% block primary %}
<div class="primary col-12" role="main">
<div class="primary col-9" role="main">
{% block primary_content %}
{{ super() }}
{% endblock %}
</div>
{% endblock primary %}

{% block secondary_content %}
<div class="module-content">
<h3>{{ _("Your chats") }}</h3>

<ul class="list-unstyled">
{% for chat in chats %}
<li>
<a href="{{ h.url_for('chatbot.chat', chat_id=chat.chat_id)}}">
{{ _("Chat") }} #{{ loop.index }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endblock secondary_content %}

{% block primary_content_inner %}
<form method="POST" action="{{ h.url_for('chatbot.talk') }}">
{{ form.textarea('promt', label=_('Promt'), value=data.promt, error=error, placeholder=_('Tell me, what do you want?')) }}

<div class="response-area">
{% for message in active_chat %}
{{ message.content }}
{% endfor %}
</div>

<p>
<span id="response">
<span id="response" style="white-space: pre-line;">
{{ response }}
</span>

<span id="spinner" class="htmx-indicator">
{% snippet 'chatbot/bars.svg' %}
</span>
</p>

<p>
<div class="input-area">
{{ form.textarea('prompt', label=_('Prompt'), value="", placeholder=_('Tell me, what do you want?')) }}
<a
href="#"
title="{{ _('Send') }}"
Expand All @@ -40,6 +54,9 @@
class="btn btn-primary">
{{ _("Send") }}
</a>
</p>
<span id="spinner" class="htmx-indicator mx-2">
{% snippet 'chatbot/bars.svg' %}
</span>
</div>
</form>
{% endblock %}
13 changes: 13 additions & 0 deletions ckanext/chatbot/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

from typing import Literal, TypedDict


class Chat(TypedDict):
chat_id: str
messages: list[Message]


class Message(TypedDict):
role: Literal["assistant"] | Literal["user"] | Literal["system"]
content: str
91 changes: 91 additions & 0 deletions ckanext/chatbot/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import annotations

import uuid
from typing import Any, cast

import ckan.types as types
import ckan.plugins.toolkit as tk

import ckanext.chatbot.const as const
from ckanext.chatbot import const, types as cb_types


def create_chat(user_id: str) -> cb_types.Chat:
flake = get_data_from_flake(const.FLAKE_CHATS.format(user_id))

new_chat = cb_types.Chat(chat_id=str(uuid.uuid4()), messages=[])

flake["data"].setdefault("chats", {})
flake["data"]["chats"][new_chat["chat_id"]] = new_chat

store_data_in_flake(const.FLAKE_CHATS.format(user_id), flake["data"])

return new_chat


def get_chat(user_id: str, chat_id: str) -> cb_types.Chat | None:
flake = get_data_from_flake(const.FLAKE_CHATS.format(user_id))

for chat in flake["data"]["chats"]:
if chat["chat_id"] == chat_id:
return chat

return None


def get_user_chats(user_id: str) -> list[cb_types.Chat]:
flake = get_data_from_flake(const.FLAKE_CHATS.format(user_id))

if "chats" not in flake["data"]:
return [create_chat(user_id)]

return list(flake["data"]["chats"].values())


def drop_chat(user_id: str, chat_id: str) -> bool:
flake = tk.get_action("flakes_flake_lookup")(
prepare_context(),
{"name": const.FLAKE_CHATS.format(user_id), "author_id": None},
)

chat = flake["data"]["chats"].pop(chat_id, None)

return bool(chat)


def add_message_to_chat(message: cb_types.Message, chat_id: str) -> None:
pass


def prepare_context() -> types.Context:
return cast(
types.Context,
{"ignore_auth": True},
)


def store_data_in_flake(flake_name: str, data: Any) -> dict[str, Any]:
"""Save the serializable data into the flakes table."""
return tk.get_action("flakes_flake_override")(
prepare_context(),
{"author_id": None, "name": flake_name, "data": data},
)


def get_data_from_flake(flake_name: str) -> dict[str, Any]:
"""Retrieve a previously stored data from the flake."""
try:
return tk.get_action("flakes_flake_lookup")(
prepare_context(),
{"author_id": None, "name": flake_name},
)
except tk.ObjectNotFound:
return tk.get_action("flakes_flake_create")(
prepare_context(),
{"author_id": None, "name": flake_name, "data": {}},
)


def before_request() -> None:
if tk.current_user.is_anonymous:
tk.abort(403, tk._("You have to be authorized"))
52 changes: 42 additions & 10 deletions ckanext/chatbot/views.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
from __future__ import annotations

import logging

import os

import requests
from openai import OpenAI
from flask import Blueprint, Response
from flask.views import MethodView

import ckan.plugins.toolkit as tk

from ckanext.chatbot import utils


log = logging.getLogger(__name__)
bp = Blueprint("chatbot", __name__)
bp.before_request(utils.before_request)

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))


class ChatBotTalkView(MethodView):

def get(self) -> str:
return tk.render("chatbot/talk.html", extra_vars={"data": {}, "errors": {}})
return tk.render(
"chatbot/talk.html",
extra_vars={"chats": utils.get_user_chats(tk.current_user.id)}, # type: ignore
)

def post(self) -> str:
promt = tk.request.form["promt"]
prompt = tk.request.form["prompt"]

if promt:
response = self.generate_response(promt)
if prompt:
response = self.generate_response(prompt)
else:
response = "Please, provide a promt"

print(response)
response = "Please, provide a prompt"

return response

Expand All @@ -39,19 +43,47 @@ def generate_response(self, prompt: str) -> str:
messages=[
{
"role": "system",
"content": "Marv is a factual chatbot that is also sarcastic",
"content": "Marv is a CKAN AI assistant bot and he answers only questions related to CKAN platform",
},
{
"role": "system",
"content": "When someone asks Marv about questions not related to CKAN, he says that he can help only with CKAN",
},
{
"role": "system",
"content": "Marv understands, that he's a CKAN AI assistant and he is on a CKAN portal",
},
{
"role": "system",
"content": "Marv understands, that user that asks a question is logged in to a CKAN portal and doesn't suggest them to log in",
},
{
"role": "system",
"content": "Marv returns a response in markdown format",
},
{
"role": "user",
"content": prompt,
},
],
model="ft:gpt-3.5-turbo-0125:personal::91xHVRq2",
max_tokens=100,
# max_tokens=100,
n=1,
)

return response.choices[0].message.content or ""


class ChatBotChatView(MethodView):
def get(self, chat_id: str) -> str:
return tk.render(
"chatbot/talk.html",
extra_vars={
"chats": utils.get_user_chats(tk.current_user.id), # type: ignore
"active_chat": utils.get_chat(tk.current_user.id, chat_id), # type: ignore
},
)


bp.add_url_rule("/chatbot/talk", view_func=ChatBotTalkView.as_view("talk"))
bp.add_url_rule("/chatbot/talk/<chat_id>", view_func=ChatBotChatView.as_view("chat"))

0 comments on commit a9b96b5

Please sign in to comment.