Skip to content

Commit

Permalink
Skill library - Skill Assistant mechanics (#297)
Browse files Browse the repository at this point in the history
This PR completes the base mechanics of the skill library and implements
it into the skill assistant. With this PR, the skill library now
supports the creation of skill packages, including actions and routines,
and allows for their registration to an assistant, as demonstrated in
the skill assistant.

To demo, create an instance of the skill assistant in the workbench and
send a "/help" message to the assistant.

Instruction routines and state machine routines are fully supported.
  • Loading branch information
payneio authored Jan 8, 2025
1 parent ebb7ede commit 68ffd7e
Show file tree
Hide file tree
Showing 53 changed files with 4,302 additions and 480 deletions.
57 changes: 6 additions & 51 deletions assistants/skill-assistant/assistant/assistant_registry.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import asyncio
from os import PathLike
from pathlib import Path
from typing import Optional

from openai_client.chat_driver import ChatDriverConfig
from skill_library import Assistant, Skill
from skill_library import Assistant

from .logging import extra_data, logger
from .skill_event_mapper import SkillEventMapperProtocol
Expand All @@ -22,32 +18,6 @@ def __init__(self) -> None:
self.assistants: dict[str, Assistant] = {}
# self.tasks: set[asyncio.Task] = set()

async def get_or_register_assistant(
self,
assistant_id: str,
event_mapper: SkillEventMapperProtocol,
chat_driver_config: ChatDriverConfig,
assistant_name: str = "Assistant",
skills: Optional[dict[str, "Skill"]] = None,
drive_root: PathLike | None = None,
metadata_drive_root: PathLike | None = None,
) -> Assistant:
"""
Get or create an assistant for the given conversation context.
"""
assistant = self.get_assistant(assistant_id)
if not assistant:
assistant = await self.register_assistant(
assistant_id,
event_mapper,
chat_driver_config,
assistant_name,
skills,
drive_root,
metadata_drive_root,
)
return assistant

def get_assistant(
self,
assistant_id: str,
Expand All @@ -58,38 +28,23 @@ def get_assistant(

async def register_assistant(
self,
assistant_id: str,
assistant: Assistant,
event_mapper: SkillEventMapperProtocol,
chat_driver_config: ChatDriverConfig,
assistant_name: str = "Assistant",
skills: dict[str, Skill] | None = None,
drive_root: PathLike | None = None,
metadata_drive_root: PathLike | None = None,
) -> Assistant:
"""
Define the skill assistant that you want to have backing this assistant
service. You can configure the assistant instructions and which skills
to include here.
"""

logger.debug("Registering assistant.", extra_data({"assistant_id": assistant_id}))

# Create the assistant.
assistant = Assistant(
name=assistant_name,
assistant_id=assistant_id,
drive_root=drive_root or Path(".data") / assistant_id / "assistant",
metadata_drive_root=metadata_drive_root or Path(".data") / assistant_id / ".assistant",
chat_driver_config=chat_driver_config,
skills=skills,
)
logger.debug("Registering assistant.", extra_data({"assistant_id": assistant.assistant_id}))

# Assistant event consumer.
async def subscribe() -> None:
"""Event consumer for the assistant."""
logger.debug(
"Assistant event subscription started in the assistant registry.",
extra_data({"assistant_id": assistant_id}),
extra_data({"assistant_id": assistant.assistant_id}),
)
async for skill_event in assistant.events:
logger.debug(
Expand All @@ -104,11 +59,11 @@ async def subscribe() -> None:
await assistant.wait()
logger.debug(
"Assistant event subscription stopped in the assistant registry.",
extra_data({"assistant_id": assistant_id}),
extra_data({"assistant_id": assistant.assistant_id}),
)

# Register the assistant.
self.assistants[assistant_id] = assistant
self.assistants[assistant.assistant_id] = assistant

# Start an event consumer task and save a reference.
# task = asyncio.create_task(subscribe())
Expand Down
7 changes: 4 additions & 3 deletions assistants/skill-assistant/assistant/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ def default(self, o):
return super().default(o)


def extra_data(data: Any) -> dict[str, Any]:
def add_serializable_data(data: Any) -> dict[str, Any]:
"""
Helper function to use when adding extra data to log messages.
Helper function to use when adding extra data to log messages. Data will
attempt to be put into a serializable format.
"""
extra = {}

Expand All @@ -58,4 +59,4 @@ def extra_data(data: Any) -> dict[str, Any]:
return extra


extra_data = extra_data
extra_data = add_serializable_data
66 changes: 41 additions & 25 deletions assistants/skill-assistant/assistant/skill_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

import openai_client
from assistant_drive import Drive, DriveConfig
from common_skill import CommonSkillDefinition
from content_safety.evaluators import CombinedContentSafetyEvaluator
from form_filler_skill import FormFillerSkill
from guided_conversation_skill import GuidedConversationSkill
from guided_conversation_skill import GuidedConversationSkillDefinition
from openai_client.chat_driver import ChatDriverConfig
from posix_skill import PosixSkill
from posix_skill import PosixSkillDefinition
from semantic_workbench_api_model.workbench_model import (
ConversationEvent,
ConversationMessage,
Expand All @@ -30,6 +30,7 @@
ContentSafetyEvaluator,
ConversationContext,
)
from skill_library import Assistant
from skill_library.types import Metadata

from assistant.skill_event_mapper import SkillEventMapper
Expand Down Expand Up @@ -58,8 +59,8 @@


# Create the content safety interceptor.
async def content_evaluator_factory(context: ConversationContext) -> ContentSafetyEvaluator:
config = await assistant_config.get(context.assistant)
async def content_evaluator_factory(conversation_context: ConversationContext) -> ContentSafetyEvaluator:
config = await assistant_config.get(conversation_context.assistant)
return CombinedContentSafetyEvaluator(config.content_safety_config)


Expand Down Expand Up @@ -178,7 +179,23 @@ async def respond_to_conversation(
"""
Respond to a conversation message.
"""
assistant = await get_or_register_assistant(conversation_context, config)
try:
await assistant.put_message(message.content, metadata)
except Exception as e:
logger.exception("Exception in on_message_created.")
await conversation_context.send_messages(
NewConversationMessage(
message_type=MessageType.note,
content=f"Unhandled error: {e}",
)
)


# Get or register an assistant for the conversation.
async def get_or_register_assistant(
conversation_context: ConversationContext, config: AssistantConfigModel
) -> Assistant:
# Get an assistant from the registry.
assistant_id = conversation_context.id
assistant = assistant_registry.get_assistant(assistant_id)
Expand All @@ -194,26 +211,32 @@ async def respond_to_conversation(
model=config.chat_driver_config.openai_model,
instructions=config.chat_driver_config.instructions,
)
assistant = await assistant_registry.register_assistant(

assistant = Assistant(
assistant_id=conversation_context.id,
assistant_name="Assistant",
event_mapper=SkillEventMapper(conversation_context),
name="Assistant",
chat_driver_config=chat_driver_config,
drive_root=assistant_drive_root,
metadata_drive_root=assistant_metadata_drive_root,
skills={
"posix": PosixSkill(
"common": CommonSkillDefinition(
name="common",
language_model=language_model,
drive=assistant_drive.subdrive("common"),
chat_driver_config=chat_driver_config,
),
"posix": PosixSkillDefinition(
name="posix",
sandbox_dir=Path(".data") / conversation_context.id,
chat_driver_config=chat_driver_config,
mount_dir="/mnt/data",
),
"form_filler": FormFillerSkill(
name="form_filler",
chat_driver_config=chat_driver_config,
language_model=language_model,
),
"guided_conversation": GuidedConversationSkill(
# "form_filler": FormFillerSkill(
# name="form_filler",
# chat_driver_config=chat_driver_config,
# language_model=language_model,
# ),
"guided_conversation": GuidedConversationSkillDefinition(
name="guided_conversation",
language_model=language_model,
drive=assistant_drive.subdrive("guided_conversation"),
Expand All @@ -222,13 +245,6 @@ async def respond_to_conversation(
},
)

try:
await assistant.put_message(message.content, metadata)
except Exception as e:
logger.exception("Exception in on_message_created.")
await conversation_context.send_messages(
NewConversationMessage(
message_type=MessageType.note,
content=f"Unhandled error: {e}",
)
)
await assistant_registry.register_assistant(assistant, SkillEventMapper(conversation_context))

return assistant
5 changes: 3 additions & 2 deletions assistants/skill-assistant/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ dependencies = [
"azure-core[aio]>=1.30.0",
"azure-identity>=1.16.0",
"content-safety>=0.1.0",
"context>=0.1.0",
"common-skill>=0.1.0",
# "document-skill>=0.1.0",
"form-filler-skill>=0.1.0",
"guided-conversation-skill>=0.1.0",
"openai-client>=0.1.0",
"openai>=1.3.9",
"posix-skill>=0.1.0",
"semantic-workbench-assistant>=0.1.0",
"bs4>=0.0.2",
]

[dependency-groups]
Expand All @@ -34,8 +35,8 @@ package = true

[tool.uv.sources]
content-safety = { path = "../../libraries/python/content-safety", editable = true }
context = { path = "../../libraries/python/context", editable = true }
# document-skill = { path = "../../libraries/python/skills/skills/document-skill", editable = true }
common-skill = { path = "../../libraries/python/skills/skills/common-skill", editable = true }
form-filler-skill = { path = "../../libraries/python/skills/skills/form-filler-skill", editable = true }
guided-conversation-skill = { path = "../../libraries/python/skills/skills/guided-conversation-skill", editable = true }
openai-client = { path = "../../libraries/python/openai-client", editable = true }
Expand Down
71 changes: 69 additions & 2 deletions assistants/skill-assistant/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions libraries/python/openai-client/openai_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from .logging import (
add_serializable_data,
extra_data,
make_completion_args_serializable,
)
from .messages import (
Expand Down Expand Up @@ -48,6 +49,7 @@
"create_assistant_message",
"create_system_message",
"create_user_message",
"extra_data",
"format_with_dict",
"format_with_liquid",
"make_completion_args_serializable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def register_function(self, function: Callable) -> None:

def register_functions(self, functions: list[Callable]) -> None:
for function in functions:
self.register_function
self.register_function(function)

# Sometimes we want to register a function to be used by both the user and
# the model.
Expand Down
Loading

0 comments on commit 68ffd7e

Please sign in to comment.