Skip to content

Commit

Permalink
adds python example assistant and recent workbench app/service improv…
Browse files Browse the repository at this point in the history
…ements (#21)
  • Loading branch information
bkrabach authored Aug 14, 2024
1 parent 830984f commit 84e292e
Show file tree
Hide file tree
Showing 21 changed files with 857 additions and 18 deletions.
7 changes: 7 additions & 0 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
- Update `middleware.py` with the Application (client) ID
- Use VS Code > Run and Debug > `Semantic Workbench` to start both the app and the service
- After launching semantic-workbench-service, go to Ports and make 3000 public
- In the VS Code terminal tab, find the semantic-workbench-app and click on the link to open the app

## Python assistant example

We have included an example Python assistant that echos the user's input and can serve as a starting point for your own assistant.

See the [Python assistant example README](../examples/python-example01/README.md) for more details.

## TODO

Expand Down
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"postCreateCommand": {
// Build and restore dependencies for key projects in the repo
"make_workbench_app": "make -C /workspaces/semanticworkbench/semantic-workbench/v1/app",
"make_workbench_service": "make -C /workspaces/semanticworkbench/semantic-workbench/v1/service"
"make_workbench_service": "make -C /workspaces/semanticworkbench/semantic-workbench/v1/service",
"make_python_examples01": "make -C /workspaces/semanticworkbench/examples/python-examples01"
},

// Configure tool-specific properties.
Expand Down
5 changes: 5 additions & 0 deletions examples/python-example01/.black.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.black]
line-length = 120
target-version = ["py310", "py311"]
preview = true
unstable = true
5 changes: 5 additions & 0 deletions examples/python-example01/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Description: Example of .env file
# Usage: Copy this file to .env and set the values

# The ASSISTANT__ prefix is used to group all the environment variables related to the assistant service.
ASSISTANT__ENABLE_DEBUG_OUTPUT=True
3 changes: 3 additions & 0 deletions examples/python-example01/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
exclude = .git,__pycache__,build,dist,venv,.venv,.env,*.egg,*.egg-info,*.egg-info/*
9 changes: 9 additions & 0 deletions examples/python-example01/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__pycache__
.pytest_cache
*.egg*
.data
.venv
venv
.env

poetry.lock
49 changes: 49 additions & 0 deletions examples/python-example01/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "semantic-workbench-app",
"cwd": "${workspaceFolder}/../../semantic-workbench/v1/app",
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"]
},
{
"type": "debugpy",
"request": "launch",
"name": "semantic-workbench-service",
"cwd": "${workspaceFolder}/../../semantic-workbench/v1/service",
"module": "semantic_workbench_service.start",
"justMyCode": false,
"consoleTitle": "semantic-workbench-service",
"args": ["--host", "0.0.0.0", "--port", "3000"]
},
{
"type": "debugpy",
"request": "launch",
"name": "python-example01 (assistant)",
"cwd": "${workspaceFolder}",
"module": "semantic_workbench_assistant.start",
"args": ["assistant.chat:app", "--port", "3003"],
"consoleTitle": "${workspaceFolderBasename}",
"envFile": "${workspaceFolder}/.env"
}
],
"compounds": [
{
"name": "semantic-workbench",
"configurations": ["semantic-workbench-app", "semantic-workbench-service"]
},
{
"name": "example assistant and semantic-workbench",
"configurations": [
"semantic-workbench-app",
"semantic-workbench-service",
"python-example01 (assistant)"
]
}
]
}
50 changes: 50 additions & 0 deletions examples/python-example01/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"black-formatter.args": ["--config", "./.black.toml"],
"editor.bracketPairColorization.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
},
"editor.guides.bracketPairs": "active",
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.formatOnSave": true,
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"flake8.args": ["--config", "./.flake8"],
"isort.args": ["--profile", "black", "--gitignore"],
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"python.analysis.autoFormatStrings": true,
"python.analysis.autoImportCompletions": true,
"python.analysis.diagnosticMode": "workspace",
"python.analysis.exclude": [
"**/.venv/**",
"**/.data/**",
"**/__pycache__/**"
],
"python.analysis.fixAll": ["source.unusedImports"],
"python.analysis.inlayHints.functionReturnTypes": true,
"python.analysis.typeCheckingMode": "basic",
"python.defaultInterpreterPath": "${workspaceFolder}/.venv",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": {
"source.unusedImports": "explicit",
"source.organizeImports": "explicit",
"source.fixAll": "explicit",
"source.formatDocument": "explicit"
}
},
"search.exclude": {
"**/.venv": true,
"**/.data": true,
"**/__pycache__": true
},
}
5 changes: 5 additions & 0 deletions examples/python-example01/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
repo_root = $(shell git rev-parse --show-toplevel)

.DEFAULT_GOAL := venv

include $(repo_root)/build-tools/makefiles/poetry.mk
17 changes: 17 additions & 0 deletions examples/python-example01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
A python chat assistant example that echos the user's input.

## Pre-requisites

- Complete the steps in either:
- [main README](../../README.md)
- [GitHub Codespaces / devcontainers README](../../.devcontainer/README.md)
- Set up and verify that the workbench app and service are running
- Stop the services and open the `python-examples01.code-workspace` in VS Code

## Steps

- Use VS Code > Run and Debug > `example assistant and semantic-workbench` to start the assistant.
- If running in a devcontainer, follow the instructions in [GitHub Codespaces / devcontainers README](../../.devcontainer/README.md) for any additional steps.
- Return to the workbench app to interact with the assistant
- Add a new assistant from the main menu of the app, choose `Python Example 01 Assistant`
- Click the newly created assistant to configure and interact with it
Empty file.
133 changes: 133 additions & 0 deletions examples/python-example01/assistant/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import logging
from typing import AsyncContextManager, Callable, TypeVar
from uuid import UUID

from semantic_workbench_api_model.workbench_model import (
ConversationEvent,
ConversationEventType,
NewConversationMessage,
)
from semantic_workbench_assistant import assistant_service
from semantic_workbench_assistant.assistant_base import (
AssistantBase,
AssistantInstance,
SimpleAssistantConfigStorage,
)

from assistant.config import AssistantConfigModel

from . import config

logger = logging.getLogger(__name__)

# Example built on top of the Assistant base
# This example demonstrates how to extend the Assistant base
# to add additional configuration fields and UI schema for the configuration fields
# and how to create a new Assistant that uses the extended configuration model

# Comments marked with "Required", "Optional", and "Custom" indicate the type of code that follows
# Required: code that is required to be implemented for any Assistant
# Optional: code that is optional to implement for any Assistant, allowing for customization
# Custom: code that was added specifically for this example

# Modify the config.py file to add any additional configuration fields
ConfigT = TypeVar("ConfigT", bound=AssistantConfigModel)

service_id = "python-example01-assistant.python-example"
service_name = "Python Example 01 Assistant"
service_description = "A starter for building a chat assistant using the Semantic Workbench Assistant SDK."


class ChatAssistant(AssistantBase):

# Optional: override the __init__ method to add any additional initialization logic
def __init__(
self,
register_lifespan_handler: Callable[[Callable[[], AsyncContextManager[None]]], None],
service_id=service_id,
service_name=service_name,
service_description=service_description,
) -> None:

super().__init__(
register_lifespan_handler=register_lifespan_handler,
service_id=service_id,
service_name=service_name,
service_description=service_description,
config_storage=SimpleAssistantConfigStorage[config.AssistantConfigModel](
cls=config.AssistantConfigModel,
default_config=config.AssistantConfigModel(),
ui_schema=config.ui_schema,
),
)

# Optional: Override the on_workbench_event method to provide custom handling of conversation events for this
# assistant
async def on_workbench_event(
self,
assistant_instance: AssistantInstance,
event: ConversationEvent,
) -> None:
# add any additional event processing logic here
match event.event:

case ConversationEventType.message_created | ConversationEventType.conversation_created:
# replace the following with your own logic for processing a message created event
return await self.respond_to_conversation(assistant_instance.id, event.conversation_id)

case _:
# add any additional event processing logic here
pass

# Custom: Implement a custom method to respond to a conversation
async def respond_to_conversation(self, assistant_id: str, conversation_id: UUID) -> None:
# get the conversation client
conversation_client = self.workbench_client.for_conversation(assistant_id, str(conversation_id))

# get the assistant's messages
messages_response = await conversation_client.get_messages()
if len(messages_response.messages) == 0:
# unexpected, no messages in the conversation
return None

# get the last message
last_message = messages_response.messages[-1]

# check if the last message was sent by this assistant
if last_message.sender.participant_id == assistant_id:
# ignore messages from this assistant
return

# get the assistant's configuration, supports overwriting defaults from environment variables
assistant_config = self._config_storage.get_with_defaults_overwritten_from_env(assistant_id)

# add your custom logic here for generating a response to the last message
# example: for now, just echo the last message back to the conversation

# send a new message with the echo response
await conversation_client.send_messages(
NewConversationMessage(
content=f"echo: {last_message.content}",
metadata=(
{
"debug": {
"source": "echo",
"original_message": last_message,
}
}
if assistant_config.enable_debug_output
else None
),
)
)

# add any additional methods or overrides here
# see assistant_base.AssistantBase for examples


# Required: Create an instance of the Assistant class
app = assistant_service.create_app(
lambda lifespan: ChatAssistant(
register_lifespan_handler=lifespan.register_handler,
)
)
39 changes: 39 additions & 0 deletions examples/python-example01/assistant/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Annotated, Self
from pydantic import Field
from semantic_workbench_assistant import config, assistant_base

# The semantic workbench app uses react-jsonschema-form for rendering
# dyanmic configuration forms based on the configuration model and UI schema
# See: https://rjsf-team.github.io/react-jsonschema-form/docs/
# Playground / examples: https://rjsf-team.github.io/react-jsonschema-form/


# the workbench app builds dynamic forms based on the configuration model and UI schema
class AssistantConfigModel(assistant_base.AssistantConfigModel):
enable_debug_output: Annotated[
bool,
Field(
title="Include Debug Output",
description="Include debug output on conversation messages.",
),
] = False

def overwrite_defaults_from_env(self) -> Self:
"""
Overwrite string fields that currently have their default values. Values are
overwritten with values from environment variables or .env file. If a field
is a BaseModel, it will be recursively updated.
"""
updated = config.overwrite_defaults_from_env(self, prefix="assistant", separator="__")

# Custom: add any additional handling for nested models

return updated

# add any additional configuration fields


ui_schema = {
# add UI schema for the additional configuration fields
# see: https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema
}
35 changes: 35 additions & 0 deletions examples/python-example01/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tool.poetry]
name = "assistant"
version = "0.1.0"
description = "Example of a python Semantic Workbench assistant."
authors = ["Semantic Workbench Team"]
readme = "README.md"
packages = [{ include = "assistant" }]

[tool.poetry.dependencies]
python = "~3.11"
openai = "^1.3.9"

semantic-workbench-api-model = { path = "../../semantic-workbench/v1/service/semantic-workbench-api-model", develop = true, extras=["dev"] }
semantic-workbench-assistant = { path = "../../semantic-workbench/v1/service/semantic-workbench-assistant", develop = true, extras=["dev"] }
semantic-workbench-service = { path = "../../semantic-workbench/v1/service/semantic-workbench-service", develop = true, extras=["dev"] }

black = { version = "^24.3.0", optional = true }
flake8 = { version = "^6.1.0", optional = true }

[tool.poetry.extras]
dev = ["black", "flake8"]

[build-system]
requires = ["poetry-core>=1.2.0"]
build-backend = "poetry.core.masonry.api"

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
line_length = 120
profile = "black"

[tool.pyright]
exclude = ["venv", ".venv"]
8 changes: 8 additions & 0 deletions examples/python-example01/python-examples01.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": ".",
"name": "examples/python-examples01"
}
]
}
Loading

0 comments on commit 84e292e

Please sign in to comment.