Skip to content

Commit

Permalink
✨ add basic agent logic
Browse files Browse the repository at this point in the history
  • Loading branch information
yanyongyu committed Mar 12, 2024
1 parent 655b9ab commit fbc1756
Show file tree
Hide file tree
Showing 18 changed files with 232 additions and 59 deletions.
17 changes: 17 additions & 0 deletions .github/actions/generate-schema/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Generate Config Schema
description: Generate the JSON schema for operagents config

inputs:
output-path:
description: Output path for the generated schema
required: false
default: "operagents/config/operagents.schema.json"

runs:
using: "composite"
steps:
- name: Generate
run: |
poetry run python ./scripts/generate_config_schema.py {{ inputs.output-path }}
echo "output-path={{ inputs.output-path }}" >> "$GITHUB_OUTPUT"
shell: bash
24 changes: 24 additions & 0 deletions .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Setup Python
description: Setup Python

inputs:
python-version:
description: Python version
required: false
default: "3.10"

runs:
using: "composite"
steps:
- name: Install poetry
run: pipx install poetry
shell: bash

- uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
architecture: "x64"
cache: "poetry"

- run: poetry install
shell: bash
60 changes: 60 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: GitHub Pages

on:
push:
branches:
- master
pull_request:

jobs:
generate-config-schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python environment
uses: ./.github/actions/setup-python

- name: Generate
id: generate-schema
uses: ./.github/actions/generate-schema

- name: Upload
uses: actions/upload-artifact@v2
with:
name: operagents-config-schema
path: ${{ steps.generate-schema.outputs.output-path }}
retention-days: 1

build:
runs-on: ubuntu-latest
needs: generate-config-schema
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
fetch-depth: 1

- name: Download Schema
uses: actions/download-artifact@v2
with:
name: operagents-config-schema
path: schemas/

- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v3

deploy:
runs-on: ubuntu-latest
needs: build
if: ${{ github.event_name == 'push' }}
permissions:
pages: write
id-token: write
environment:
name: website
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy GitHub Pages site
id: deployment
uses: actions/deploy-pages@v4
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

.idea
.vscode
operagents/config/*.schema.json

# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
Expand Down Expand Up @@ -143,7 +144,6 @@ fabric.properties
# Icon must end with two \r
Icon


# Thumbnails
._*

Expand Down
18 changes: 13 additions & 5 deletions examples/chatbot/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,36 @@ agents:
backend:
type: user
system_template: ""
user_template: ""
user_template: |-
{% for event in timeline.past_events(agent) -%}
{% if event.type_ == "act" -%}
[Scene {{ event.scene.name}}] {{ event.character.agent_name }}({{ event.character.name }}): {{ event.content }}
{%- endif %}
{%- endfor %}
John:
backend:
type: openai
model: gpt-3.5-turbo-16k-0613
temperature: 0.3
system_template: |-
Your name is {{ agent.name }}.
Current scene is {{ timeline.current_scene.name }}.
{% if timeline.current_scene.description -%}
{{ timeline.current_scene.description }}
{%- endif %}
You are acting as {{ timeline.current_character.agent_name }}.
{%- endif -%}
You are acting as {{ timeline.current_character.name }}.
{% if timeline.current_character.description -%}
{{ timeline.current_character.description }}
{%- endif %}
Please continue the conversation on behalf of {{ agent.name }} based on your known information.
{%- endif -%}
user_template: |-
Here is your chat history:
{% for event in timeline.past_events(agent) -%}
{% if event.type_ == "act" -%}
[Scene {{ event.scene.name}}] {{ event.character.agent_name }}({{ event.character.name }}): {{ event.content }}
{%- endif %}
{%- endfor %}
Please continue the conversation on behalf of {{ agent.name }}({{ timeline.current_character.name }}) based on your known information and make your answer appear as natural and coherent as possible.
Please answer directly what you want to say and keep your reply as concise as possible.
opening_scene: talking

Expand Down
19 changes: 0 additions & 19 deletions examples/chatbot/run.py

This file was deleted.

27 changes: 16 additions & 11 deletions operagents/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from operagents.config import AgentConfig, TemplateConfig

if TYPE_CHECKING:
from operagents.backend import Backend
from operagents.timeline import Timeline
from operagents.backend import Backend, Message
from operagents.timeline.event import TimelineEvent


Expand All @@ -29,7 +29,7 @@ class Agent:
user_template: TemplateConfig = field(kw_only=True)
"""The user template to use for generating text."""

chat_history: list = field(default_factory=list, kw_only=True)
chat_history: list["Message"] = field(default_factory=list, kw_only=True)
"""The history of the agent self's chat messages."""

def __post_init__(self):
Expand All @@ -49,13 +49,13 @@ def from_config(cls, name: str, config: AgentConfig) -> Self:

async def act(self, timeline: "Timeline") -> "TimelineEvent":
"""Make the agent act."""
system_message = await self.system_renderer.render_async(
agent=self, timeline=timeline
)
new_message = await self.user_renderer.render_async(
agent=self, timeline=timeline
)
messages = [
system_message = (
await self.system_renderer.render_async(agent=self, timeline=timeline)
).strip()
new_message = (
await self.user_renderer.render_async(agent=self, timeline=timeline)
).strip()
messages: list["Message"] = [
{
"role": "system",
"content": system_message,
Expand All @@ -66,9 +66,11 @@ async def act(self, timeline: "Timeline") -> "TimelineEvent":
"content": new_message,
},
]
await self.logger.adebug("Acting", messages=messages)
await self.logger.adebug(
"Acting", character=timeline.current_character, messages=messages
)
response = await self.backend.generate(messages)
await self.logger.ainfo(response)
await self.logger.ainfo(response, character=timeline.current_character)

self.chat_history.append(
{
Expand All @@ -88,3 +90,6 @@ async def act(self, timeline: "Timeline") -> "TimelineEvent":
scene=timeline.current_scene,
content=response,
)

# TODO: Implement this
async def observe(self, timeline: "Timeline"): ...
4 changes: 4 additions & 0 deletions operagents/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
from operagents.utils import get_all_subclasses, resolve_dot_notation

from ._base import Backend as Backend
from ._base import Message as Message
from .user import UserBackend as UserBackend
from ._base import UserMessage as UserMessage
from ._base import SystemMessage as SystemMessage
from .openai import OpenAIBackend as OpenAIBackend
from ._base import AssistantMessage as AssistantMessage

all_backend_types: dict[str, type[Backend]] = {
b.type_: b for b in get_all_subclasses(Backend)
Expand Down
56 changes: 53 additions & 3 deletions operagents/backend/_base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import abc
from typing import ClassVar
from typing import Literal, ClassVar, TypeAlias
from typing_extensions import Required, TypedDict


class Function(TypedDict):
name: str
"""The name of the function to call."""
arguments: str
"""
The arguments to call the function with, as generated by the model in JSON
format. Note that the model does not always generate valid JSON, and may
hallucinate parameters not defined by your function schema. Validate the
arguments in your code before calling your function.
"""


class MessageToolCall(TypedDict):
type: Literal["function"]
"""The type of the tool. Currently, only `function` is supported."""
id: str
"""The ID of the tool call."""
function: Function
"""The function that the model called."""


class SystemMessage(TypedDict):
role: Literal["system"]
"""The role of the messages author, in this case `system`."""
content: str
"""The contents of the system message."""


class UserMessage(TypedDict):
role: Literal["user"]
"""The role of the messages author, in this case `user`."""
content: str
"""The contents of the user message."""


class AssistantMessage(TypedDict, total=False):
role: Required[Literal["assistant"]]
"""The role of the messages author, in this case `assistant`."""
content: str | None
"""The contents of the assistant message.
Required unless `tool_calls` is specified.
"""
tool_calls: list[MessageToolCall]
"""The tool calls generated by the model, such as function calls."""


Message: TypeAlias = SystemMessage | UserMessage | AssistantMessage


class Backend(abc.ABC):
type_: ClassVar[str]

# TODO: messages
@abc.abstractmethod
async def generate(self, messages: list) -> str:
async def generate(self, messages: list[Message]) -> str:
raise NotImplementedError
12 changes: 7 additions & 5 deletions operagents/backend/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

import openai

from ._base import Backend
from ._base import Backend, Message


class OpenAIBackend(Backend):
type_ = "openai"

def __init__(self, model: str) -> None:
def __init__(self, model: str, temperature: float | None = None) -> None:
super().__init__()

self.client = openai.AsyncOpenAI()
self.model: str = model
self.temperature: float | None = temperature

@override
async def generate(self, messages: list) -> str:
# TODO: messages
async def generate(self, messages: list[Message]) -> str:
response = await self.client.chat.completions.create(
model=self.model, messages=messages
model=self.model,
temperature=self.temperature,
messages=messages, # type: ignore
)
reply = response.choices[0].message.content
if reply is None:
Expand Down
15 changes: 12 additions & 3 deletions operagents/backend/user.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
from typing_extensions import override

from ._base import Backend
from noneprompt import InputPrompt, CancelledError

from operagents.log import logger
from operagents.exception import OperaFinished

from ._base import Backend, Message


class UserBackend(Backend):
type_ = "user"

@override
async def generate(self, messages: list) -> str:
return input("You: ")
async def generate(self, messages: list[Message]) -> str:
try:
return await InputPrompt("You: ").prompt_async()
except CancelledError:
await logger.ainfo("User cancelled input.")
raise OperaFinished() from None
Loading

0 comments on commit fbc1756

Please sign in to comment.