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

feat: Add shiny assistant app #375

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,8 @@ cython_debug/

/.luarc.json
_dev/


# LLM prompt generation
/typings/
/repomix-output.*
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ lint: dev
$(UV) run ruff check
$(UV) run pyright

shiny: dev
$(MAKE) -C ./shiny assistant

test: dev
$(UV) run coverage run --source=src -m pytest tests
$(UV) run --no-group llm coverage run --source=src -m pytest tests

uninstall: ensure-uv
$(UV) pip uninstall $(PROJECT_NAME)
Expand All @@ -93,6 +96,7 @@ help:
@echo " install Install the built project"
@echo " it Run integration tests"
@echo " lint Lint the code"
@echo " shiny Update compiled files for Shiny apps"
@echo " test Run unit tests with coverage"
@echo " uninstall Uninstall the project"
@echo " version Display the project version"
2 changes: 1 addition & 1 deletion examples/connect/databricks/shiny/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from databricks import sql
from databricks.sdk.core import ApiClient, Config, databricks_cli
from databricks.sdk.service.iam import CurrentUserAPI
from shiny import App, Inputs, Outputs, Session, render, ui

from posit.connect.external.databricks import PositCredentialsStrategy
from shiny import App, Inputs, Outputs, Session, render, ui

DATABRICKS_HOST = os.getenv("DATABRICKS_HOST")
DATABRICKS_HOST_URL = f"https://{DATABRICKS_HOST}"
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ examples = ["rsconnect-python", "pandas", "databricks", "shiny"]
git = ["pre-commit"]
lint = ["ruff", "pyright"]
test = ["rsconnect-python", "responses", "pytest", "pyjson5"]
llm = [
"chatlas@git+https://github.com/posit-dev/chatlas@main; python_version>='3.9'",
"anthropic[bedrock]",
"shiny",
"faicons",
]
extra = ["pytz"]
# Default install group by `uv`: `dev`
dev = [
{ include-group = "build" },
Expand All @@ -146,4 +153,6 @@ dev = [
{ include-group = "git" },
{ include-group = "lint" },
{ include-group = "test" },
# { include-group = "llm" },
{ include-group = "extra" },
]
32 changes: 32 additions & 0 deletions shiny/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
include ../vars.mk

.DEFAULT_GOAL := all

.PHONY: deploy assistant assistant-update-prompt assistant-test-chat help

all: assistant-update-prompt

SHINY_DIRS:=$(subst .,,$(subst /,,$(dir $(wildcard ./*/app.py))))
deploy:
$(foreach dir,$(SHINY_DIRS),$(MAKE) deploy-$(dir);)
deploy-%:
cd "$*" && \
$(UV) run --group llm rsconnect deploy shiny . -v

assistant:
$(MAKE) assistant-update-prompt
assistant-update-prompt:
cd assistant && \
$(UV) run --group llm python _update_prompt.py
assistant-test-chat:
cd assistant && \
$(UV) run --group llm python _test_chat.py

help:
@echo "Makefile Targets"
@echo " all Run assistant-update-prompt"
@echo " assistant Run assistant-update-prmopt"
@echo " assistant-update-prompt Update the assistant prompt"
@echo " assistant-test-chat Test the assistant chat"
@echo " deploy Deploys each Shiny app"
@echo " deploy-<APPDIR> Deploys the <APPDIR> Shiny app"
6 changes: 6 additions & 0 deletions shiny/assistant/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
chatlas/
_prompt.xml
rsconnect-python/
_swagger.json
_swagger_prompt.md
_repomix-instructions.md
242 changes: 242 additions & 0 deletions shiny/assistant/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import os
import pathlib
import tempfile
import urllib.parse

import chatlas
import faicons

from shiny import App, Inputs, reactive, render, session, ui

app_ui = ui.page_fillable(
ui.h1(
"SDK Assistant",
ui.input_action_link("info_link", label=None, icon=faicons.icon_svg("circle-info")),
ui.output_text("cost", inline=True),
),
ui.output_ui("new_gh_issue", inline=True),
ui.chat_ui("chat", placeholder="Ask your posit-SDK questions here..."),
ui.tags.style(
"""
#info_link {
font-size: medium;
vertical-align: super;
margin-left: 10px;
}
#cost {
color: lightgrey;
font-size: medium;
vertical-align: middle;
}
.sdk_suggested_prompt {
cursor: pointer;
border-radius: 0.5em;
display: list-item;
}
.external-link {
cursor: alias;
}
#new_gh_issue {
position: absolute;
right: 15px;
top: 15px;
height: 25px;
}
"""
),
ui.tags.script(
"""
$(() => {
$("body").click(function(e) {
if (!$(e.target).hasClass("sdk_suggested_prompt")) {
return;
}
window.Shiny.setInputValue("new_sdk_prompt", $(e.target).text());
});
})
window.Shiny.addCustomMessageHandler("submit-chat", function(message) {
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
});

// Dispatch the 'Enter' event on the input element
console.log("Dispatching Enter event", message);
document.querySelector("#" + message['id'] + " textarea#chat_user_input").dispatchEvent(enterEvent);
});

"""
),
fillable_mobile=True,
)


def server(input: Inputs): # noqa: A002
aws_model = os.getenv("AWS_MODEL", "us.anthropic.claude-3-5-sonnet-20241022-v2:0")
aws_region = os.getenv("AWS_REGION", "us-east-1")
chat = chatlas.ChatBedrockAnthropic(model=aws_model, aws_region=aws_region)
prompt_file = pathlib.Path(__file__).parent / "_prompt.xml"
if not os.path.exists(prompt_file):
raise FileNotFoundError(
f"Prompt file not found: {prompt_file} ; Please run `make shiny` to generate it."
)
with open(prompt_file, "r") as f:
chat.system_prompt = f.read()

chat_ui = ui.Chat(
"chat",
# messages=[{"role": turn.role, "content": turn.text} for turn in chat.get_turns()],
)

async def submit_chat(new_value: str):
chat_ui.update_user_input(value=new_value)

local_session = session.require_active_session(None)
await local_session.send_custom_message("submit-chat", {"id": "chat"})

@render.text
def cost():
_ = chat_ui.messages()

tokens = chat.tokens("cumulative")
if len(tokens) == 0:
return None

cost = sum(
[
# Input + Output
(token[0] * 0.003 / 1000.0) + (token[1] * 0.015 / 1000.0)
for token in tokens
if token is not None
]
)
ans = "$%s" % float("%.3g" % cost)
while len(ans) < 5:
ans = ans + "0"
return ans

@render.ui
def new_gh_issue():
messages = chat_ui.messages()
for message in messages:
if message["role"] == "assistant":
break
else:
# No LLM response found. Return
return

first_message_content: str = str(messages[0].get("content", ""))

with tempfile.TemporaryDirectory() as tmpdirname:
export_path = tmpdirname + "/chat_export.md"
chat.export(export_path, include="all", include_system_prompt=False)

with open(export_path, "r") as f:
exported_content = f.read()

body = f"""
**First message:**
```
{first_message_content}
```

**Desired outcome:**

Please describe what you would like to achieve in `posit-sdk`. Any additional context, code, or examples are welcome!

```python
from posit.connect import Client
client = Client()

# Your code here
```

-----------------------------------------------

<details>
<summary>Chat Log</summary>

````markdown
{exported_content}
````
</details>
"""

title = (
"SDK Assistant: `"
+ (
first_message_content
if len(first_message_content) <= 50
else (first_message_content[:50] + "...")
)
+ "`"
)

new_issue_url = (
"https://github.com/posit-dev/posit-sdk-py/issues/new?"
+ urllib.parse.urlencode(
{
"title": title,
"labels": ["template idea"],
"body": body,
}
)
)

# if chat_ui.messages(format="anthropic")
return ui.a(
ui.img(src="new_gh_issue.svg", alt="New GitHub Issue", height="100%"),
title="Submit script example to Posit SDK",
class_="external-link",
href=new_issue_url,
target="_blank",
)

@chat_ui.on_user_submit
async def _():
user_input = chat_ui.user_input()
if user_input is None:
return
await chat_ui.append_message_stream(
await chat.stream_async(
user_input,
echo="all",
)
)

@reactive.effect
@reactive.event(input.new_sdk_prompt)
async def _():
await submit_chat(input.new_sdk_prompt())

@reactive.effect
async def _init_chat_on_load():
await submit_chat("What are the pieces of Posit connect and how do they fit together?")

# Remove the effect after the first run
_init_chat_on_load.destroy()

@reactive.effect
@reactive.event(input.info_link)
async def _():
modal = ui.modal(
ui.h1("Information"),
ui.h3("Model"),
ui.pre(
f"Model: {aws_model}\nRegion: {aws_region}",
),
ui.h3("System prompt"),
ui.pre(chat.system_prompt),
easy_close=True,
size="xl",
)
ui.modal_show(modal)


app = App(
app_ui,
server,
static_assets=pathlib.Path(__file__).parent / "www",
)
9 changes: 9 additions & 0 deletions shiny/assistant/custom-prompt-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
You are an assistant that can create Posit SDK python code that can provide code solutions to interact with the user's local Posit Connect instance.

All of your answers need to be code based. When returning answers, please restate the question and then provide the code within a code block. Err on the side of simplicity in your code answers. Be ok with asking to increase or decrease the complexity.

This is a serious exercise. Please provide evidence for each answer and double check the answers for accuracy. If a question cannot be answered using the materials and tools provided, please explicitly say so.

If a question is unclear, please ask for clarification.

If you feel there is an opportunity for further exploration, please suggest the prompts. Wrap each suggested prompt within a <a class="sdk_suggested_prompt"></a> tag.
12 changes: 12 additions & 0 deletions shiny/assistant/dev_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Notes

Possible default prompts:
* What are the pieces of Posit connect and how do they fit together?
* Can you create a sequence diagram for the typical workflow?


# Possible TODOs

* Provide common workflow examples for different User types:
* How to publish content (publisher)
* How to add a user to a group (admin)
25 changes: 25 additions & 0 deletions shiny/assistant/repomix.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"output": {
"style": "xml",
"headerText": "",
"instructionFilePath": "shiny/assistant/_repomix-instructions.md",
"fileSummary": true,
"directoryStructure": true,
"removeComments": false,
"removeEmptyLines": true,
"showLineNumbers": false,
"copyToClipboard": false,
"topFilesLength": 5,
"includeEmptyDirectories": false
},
"ignore": {
"useGitignore": false,
"useDefaultPatterns": true
},
"security": {
"enableSecurityCheck": true
},
"tokenCount": {
"encoding": "o200k_base"
}
}
Loading
Loading