Prep 0.12.0 (#26)

* no " or * just treat as spoken words

* chromadb perist to db

* collect name should contain embedding so switching between chromadb configurations doesn't brick your scenes

* fix save-as long term memory transfer

* add chroma

* director agent refactor

* tweak director command, prompt reset, ux display

* tweak director message ux

* allow clearing of prompt log

* remove auto adding of quotes if neither quote or * are present

* command to reset long term memory for the scene

* improve summarization template as it would cause some llms to add extra details

* rebuilding history will now also rebuild long term memory

* direct scene template

* fix scene time reset

* dialogue template tweaks

* better dialog format fixing

* some dialogue template adjustments

* adjust default values of director agent

* keep track of scene saved/unsaved status and confirm loading a different scene if current scene is unsaved

* prompt fixes

* remove the collection on recommitting the seen to memory, as the embeddings may have changed

* change to the official python api for the openai client and make it async

* prompt tweaks

* world state prompt parsing fixes

* improve handling of json responses

* 0 seconds ago changed to moments ago

* move memory context closer to scene

* token counts for openai client

* narrator agent option: narrate passage of time

* gitignore

* remove memory id

* refactor world state with persistence to chromadb (wip)

* remove world state update instructions

* dont display blank emotion in world state

* openai gpt-4 turbo support

* conversation agent extra instructions

* track prompt response times

* Yi and UtopiaXL

* long term memory retrieval improvements during conversations

* narrate scene tweaks

* conversation ltm augment tweaks

* hide subconfig if parent config isnt enabled

* ai assisted memory recall during conversation default to off

* openai json_object coersion only on model that supports it

openai client emit prompt processing time

* 0.12.0

* remove prompt number from prompt debug list

* add prompt number back in but shift it to the upper row

* narrate time passage hard content limit restriction for now as gpt-4
would just write a whole chapter.

* relock
This commit is contained in:
FInalWombat
2023-11-10 22:45:50 +02:00
committed by GitHub
parent 91f228aa68
commit 72202dee02
70 changed files with 1818 additions and 1683 deletions

5
.gitignore vendored
View File

@@ -6,3 +6,8 @@
*.internal*
*_internal*
talemate_env
chroma
scenes
config.yaml
!scenes/infinity-quest/assets
!scenes/infinity-quest/infinity-quest.json

1222
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "talemate"
version = "0.11.1"
version = "0.12.0"
description = "AI-backed roleplay and narrative tools"
authors = ["FinalWombat"]
license = "GNU Affero General Public License v3.0"
@@ -17,7 +17,7 @@ black = "*"
rope = "^0.22"
isort = "^5.10"
jinja2 = "^3.0"
openai = "*"
openai = ">=1"
requests = "^2.26"
colorama = ">=0.4.6"
Pillow = "^9.5"
@@ -28,7 +28,6 @@ typing_extensions = "^4.5.0"
uvicorn = "^0.23"
blinker = "^1.6.2"
pydantic = "<3"
langchain = ">0.0.213"
beautifulsoup4 = "^4.12.2"
python-dotenv = "^1.0.0"
websockets = "^11.0.3"
@@ -37,6 +36,7 @@ runpod = "==1.2.0"
nest_asyncio = "^1.5.7"
isodate = ">=0.6.1"
thefuzz = ">=0.20.0"
tiktoken = ">=0.5.1"
# ChromaDB
chromadb = ">=0.4,<1"

View File

@@ -2,4 +2,4 @@ from .agents import Agent
from .client import TextGeneratorWebuiClient
from .tale_mate import *
VERSION = "0.11.1"
VERSION = "0.12.0"

View File

@@ -10,22 +10,29 @@ from blinker import signal
import talemate.instance as instance
import talemate.util as util
from talemate.emit import emit
from talemate.events import GameLoopStartEvent
import talemate.emit.async_signals
import dataclasses
import pydantic
import structlog
__all__ = [
"Agent",
"set_processing",
]
log = structlog.get_logger("talemate.agents.base")
class AgentActionConfig(pydantic.BaseModel):
type: str
label: str
description: str = ""
value: Union[int, float, str, bool]
default_value: Union[int, float, str, bool] = None
max: Union[int, float, None] = None
min: Union[int, float, None] = None
step: Union[int, float, None] = None
scope: str = "global"
class AgentAction(pydantic.BaseModel):
enabled: bool = True
@@ -161,6 +168,33 @@ class Agent(ABC):
except AttributeError:
pass
async def on_game_loop_start(self, event:GameLoopStartEvent):
"""
Finds all ActionConfigs that have a scope of "scene" and resets them to their default values
"""
if not getattr(self, "actions", None):
return
for _, action in self.actions.items():
if not action.config:
continue
for _, config in action.config.items():
if config.scope == "scene":
# if default_value is None, just use the `type` of the current
# value
if config.default_value is None:
default_value = type(config.value)()
else:
default_value = config.default_value
log.debug("resetting config", config=config, default_value=default_value)
config.value = default_value
await self.emit_status()
async def emit_status(self, processing: bool = None):
# should keep a count of processing requests, and when the
@@ -195,6 +229,8 @@ class Agent(ABC):
def connect(self, scene):
self.scene = scene
talemate.emit.async_signals.get("game_loop_start").connect(self.on_game_loop_start)
def clean_result(self, result):
if "#" in result:

View File

@@ -6,6 +6,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Optional, Union
import talemate.client as client
import talemate.instance as instance
import talemate.util as util
import structlog
from talemate.emit import emit
@@ -30,6 +31,7 @@ class ConversationAgentEmission(AgentEmission):
generation: list[str]
talemate.emit.async_signals.register(
"agent.conversation.before_generate",
"agent.conversation.generated"
)
@@ -79,6 +81,12 @@ class ConversationAgent(Agent):
min=32,
max=512,
step=32,
),#
"instructions": AgentActionConfig(
type="text",
label="Instructions",
value="1-3 sentences.",
description="Extra instructions to give the AI for dialog generatrion.",
),
"jiggle": AgentActionConfig(
type="number",
@@ -116,6 +124,19 @@ class ConversationAgent(Agent):
),
}
),
"use_long_term_memory": AgentAction(
enabled = True,
label = "Long Term Memory",
description = "Will augment the conversation prompt with long term memory.",
config = {
"ai_selected": AgentActionConfig(
type="bool",
label="AI Selected",
description="If enabled, the AI will select the long term memory to use. (will increase how long it takes to generate a response)",
value=False,
),
}
),
}
def connect(self, scene):
@@ -301,10 +322,7 @@ class ConversationAgent(Agent):
insert_bot_token=10
)
memory = await self.build_prompt_default_memory(
scene, long_term_memory_budget,
scene_and_dialogue + [f"{character.name}: {character.description}" for character in scene.get_characters()]
)
memory = await self.build_prompt_default_memory(character)
main_character = scene.main_character.character
@@ -327,6 +345,10 @@ class ConversationAgent(Agent):
except IndexError:
director_message = False
extra_instructions = ""
if self.actions["generation_override"].enabled:
extra_instructions = self.actions["generation_override"].config["instructions"].value
prompt = Prompt.get("conversation.dialogue", vars={
"scene": scene,
"max_tokens": self.client.max_token_length,
@@ -339,12 +361,13 @@ class ConversationAgent(Agent):
"talking_character": character,
"partial_message": char_message,
"director_message": director_message,
"extra_instructions": extra_instructions,
})
return str(prompt)
async def build_prompt_default_memory(
self, scene: Scene, budget: int, existing_context: list
self, character: Character
):
"""
Builds long term memory for the conversation prompt
@@ -357,30 +380,36 @@ class ConversationAgent(Agent):
Also it will only add information that is not already in the existing context.
"""
memory = scene.get_helper("memory").agent
if not memory:
if not self.actions["use_long_term_memory"].enabled:
return []
if self.current_memory_context:
return self.current_memory_context
self.current_memory_context = []
self.current_memory_context = ""
# feed the last 3 history message into multi_query
history_length = len(scene.history)
i = history_length - 1
while i >= 0 and i >= len(scene.history) - 3:
self.current_memory_context += await memory.multi_query(
[scene.history[i]],
filter=lambda x: x
not in self.current_memory_context + existing_context,
if self.actions["use_long_term_memory"].config["ai_selected"].value:
history = self.scene.context_history(min_dialogue=3, max_dialogue=15, keep_director=False, sections=False, add_archieved_history=False)
text = "\n".join(history)
world_state = instance.get_agent("world_state")
log.debug("conversation_agent.build_prompt_default_memory", direct=False)
self.current_memory_context = await world_state.analyze_text_and_extract_context(
text, f"continue the conversation as {character.name}"
)
i -= 1
else:
history = self.scene.context_history(min_dialogue=3, max_dialogue=3, keep_director=False, sections=False, add_archieved_history=False)
log.debug("conversation_agent.build_prompt_default_memory", history=history, direct=True)
memory = instance.get_agent("memory")
context = await memory.multi_query(history, max_tokens=500, iterate=5)
self.current_memory_context = "\n".join(context)
return self.current_memory_context
async def build_prompt(self, character, char_message: str = ""):
fn = self.build_prompt_default
@@ -423,6 +452,9 @@ class ConversationAgent(Agent):
character = actor.character
emission = ConversationAgentEmission(agent=self, generation="", actor=actor, character=character)
await talemate.emit.async_signals.get("agent.conversation.before_generate").send(emission)
self.set_generation_overrides()
result = await self.client.send_prompt(await self.build_prompt(character))
@@ -473,9 +505,6 @@ class ConversationAgent(Agent):
# Remove "{character.name}:" - all occurences
total_result = total_result.replace(f"{character.name}:", "")
if total_result.count("*") % 2 == 1:
total_result += "*"
# Check if total_result starts with character name, if not, prepend it
if not total_result.startswith(character.name):
total_result = f"{character.name}: {total_result}"

View File

@@ -8,10 +8,12 @@ from typing import TYPE_CHECKING, Callable, List, Optional, Union
import talemate.util as util
from talemate.emit import wait_for_input, emit
import talemate.emit.async_signals
from talemate.prompts import Prompt
from talemate.scene_message import NarratorMessage, DirectorMessage
from talemate.automated_action import AutomatedAction
import talemate.automated_action as automated_action
from talemate.agents.conversation import ConversationAgentEmission
from .registry import register
from .base import set_processing, AgentAction, AgentActionConfig, Agent
@@ -26,11 +28,13 @@ class DirectorAgent(Agent):
verbose_name = "Director"
def __init__(self, client, **kwargs):
self.is_enabled = True
self.is_enabled = False
self.client = client
self.next_direct = 0
self.actions = {
"direct": AgentAction(enabled=False, label="Direct", description="Will attempt to direct the scene. Runs automatically after AI dialogue (n turns).", config={
"turns": AgentActionConfig(type="number", label="Turns", description="Number of turns to wait before directing the sceen", value=10, min=1, max=100, step=1)
"direct": AgentAction(enabled=True, label="Direct", description="Will attempt to direct the scene. Runs automatically after AI dialogue (n turns).", config={
"turns": AgentActionConfig(type="number", label="Turns", description="Number of turns to wait before directing the sceen", value=5, min=1, max=100, step=1),
"prompt": AgentActionConfig(type="text", label="Instructions", description="Instructions to the director", value="", scope="scene")
}),
}
@@ -46,316 +50,57 @@ class DirectorAgent(Agent):
def experimental(self):
return True
def get_base_prompt(self, character: Character, budget:int):
return [character.description, character.base_attributes.get("scenario_context", "")] + self.scene.context_history(budget=budget, keep_director=False)
def connect(self, scene):
super().connect(scene)
talemate.emit.async_signals.get("agent.conversation.before_generate").connect(self.on_conversation_before_generate)
async def on_conversation_before_generate(self, event:ConversationAgentEmission):
log.info("on_conversation_before_generate", director_enabled=self.enabled)
if not self.enabled:
return
async def decide_action(self, character: Character, goal_override:str=None):
await self.direct_scene(event.character)
"""
Pick an action to perform to move the story towards the current story goal
"""
async def direct_scene(self, character: Character):
current_goal = goal_override or await self.select_goal(self.scene)
current_goal = f"Current story goal: {current_goal}" if current_goal else current_goal
if not self.actions["direct"].enabled:
log.info("direct_scene", skip=True, enabled=self.actions["direct"].enabled)
return
response, action_eval, prompt = await self.decide_action_analyze(character, current_goal)
# action_eval will hold {'narrate': N, 'direct': N, 'watch': N, ...}
# where N is a number, action with the highest number wins, default action is watch
# if there is no clear winner
prompt = self.actions["direct"].config["prompt"].value
watch_action = action_eval.get("watch", 0)
action = max(action_eval, key=action_eval.get)
if not prompt:
log.info("direct_scene", skip=True, prompt=prompt)
return
if action_eval[action] <= watch_action:
action = "watch"
if self.next_direct % self.actions["direct"].config["turns"].value != 0 or self.next_direct == 0:
log.info("decide_action", action=action, action_eval=action_eval)
log.info("direct_scene", skip=True, next_direct=self.next_direct)
self.next_direct += 1
return
return response, current_goal, action
self.next_direct = 0
await self.direct_character(character, prompt)
async def decide_action_analyze(self, character: Character, goal:str):
@set_processing
async def direct_character(self, character: Character, prompt:str):
prompt = Prompt.get("director.decide-action-analyze", vars={
response = await Prompt.request("director.direct-scene", self.client, "director", vars={
"max_tokens": self.client.max_token_length,
"scene": self.scene,
"current_goal": goal,
"prompt": prompt,
"character": character,
})
response, evaluation = await prompt.send(self.client, kind="director")
log.info("question_direction", response=response)
return response, evaluation, prompt
@set_processing
async def direct(self, character: Character, goal_override:str=None):
analysis, current_goal, action = await self.decide_action(character, goal_override=goal_override)
if action == "watch":
return None
if action == "direct":
return await self.direct_character_with_self_reflection(character, analysis, goal_override=current_goal)
if action.startswith("narrate"):
narration_type = action.split(":")[1]
direct_narrative = await self.direct_narrative(analysis, narration_type=narration_type, goal=current_goal)
if direct_narrative:
narrator = self.scene.get_helper("narrator").agent
narrator_response = await narrator.progress_story(direct_narrative)
if not narrator_response:
return None
narrator_message = NarratorMessage(narrator_response, source="progress_story")
self.scene.push_history(narrator_message)
emit("narrator", narrator_message)
return True
@set_processing
async def direct_narrative(self, analysis:str, narration_type:str="progress", goal:str=None):
if goal is None:
goal = await self.select_goal(self.scene)
prompt = Prompt.get("director.direct-narrative", vars={
"max_tokens": self.client.max_token_length,
"scene": self.scene,
"narration_type": narration_type,
"analysis": analysis,
"current_goal": goal,
})
response = await prompt.send(self.client, kind="director")
response = response.strip().split("\n")[0].strip()
if not response:
return None
response += f" (current story goal: {prompt})"
return response
@set_processing
async def direct_character_with_self_reflection(self, character: Character, analysis:str, goal_override:str=None):
max_retries = 3
num_retries = 0
keep_direction = False
response = None
self_reflection = None
while num_retries < max_retries:
response, direction_prompt = await self.direct_character(
character,
analysis,
goal_override=goal_override,
previous_direction=response,
previous_direction_feedback=self_reflection
)
keep_direction, self_reflection = await self.direct_character_self_reflect(
response, character, goal_override, direction_prompt
)
if keep_direction:
break
num_retries += 1
log.info("direct_character_with_self_reflection", response=response, keep_direction=keep_direction)
if not keep_direction:
return None
#character_agreement = f" *{character.name} agrees with the director and progresses the story accordingly*"
#
#if "accordingly" not in response:
# response += character_agreement
#
#response = await self.transform_character_direction_to_inner_monologue(character, response)
return response
@set_processing
async def transform_character_direction_to_inner_monologue(self, character:Character, direction:str):
inner_monologue = await Prompt.request(
"conversation.direction-to-inner-monologue",
self.client,
"conversation_long",
vars={
"max_tokens": self.client.max_token_length,
"scene": self.scene,
"character": character,
"director_instructions": direction,
}
)
return inner_monologue
log.info("direct_scene", response=response)
@set_processing
async def direct_character(
self,
character: Character,
analysis:str,
goal_override:str=None,
previous_direction:str=None,
previous_direction_feedback:str=None,
):
"""
Direct the scene
"""
message = DirectorMessage(response, source=character.name)
emit("director", message, character=character)
if goal_override:
current_goal = goal_override
else:
current_goal = await self.select_goal(self.scene)
if current_goal and not current_goal.startswith("Current story goal: "):
current_goal = f"Current story goal: {current_goal}"
prompt = Prompt.get("director.direct-character", vars={
"max_tokens": self.client.max_token_length,
"scene": self.scene,
"character": character,
"current_goal": current_goal,
"previous_direction": previous_direction,
"previous_direction_feedback": previous_direction_feedback,
"analysis": analysis,
})
response = await prompt.send(self.client, kind="director")
response = response.strip().split("\n")[0].strip()
log.info(
"direct_character",
direction=response,
previous_direction=previous_direction,
previous_direction_feedback=previous_direction_feedback
)
if not response:
return None
if not response.startswith(prompt.prepared_response):
response = prompt.prepared_response + response
return response, "\n".join(prompt.as_list[:-1])
@set_processing
async def direct_character_self_reflect(self, direction:str, character: Character, goal:str, direction_prompt:Prompt) -> (bool, str):
change_matches = ["change", "retry", "alter", "reconsider"]
prompt = Prompt.get("director.direct-character-self-reflect", vars={
"direction_prompt": str(direction_prompt),
"direction": direction,
"analysis": await self.direct_character_analyze(direction, character, goal, direction_prompt),
"character": character,
"scene": self.scene,
"max_tokens": self.client.max_token_length,
})
response = await prompt.send(self.client, kind="director")
parse_choice = response[len(prompt.prepared_response):].lower().split(" ")[0]
keep = not parse_choice in change_matches
log.info("direct_character_self_reflect", keep=keep, response=response, parsed=parse_choice)
return keep, response
@set_processing
async def direct_character_analyze(self, direction:str, character: Character, goal:str, direction_prompt:Prompt):
prompt = Prompt.get("director.direct-character-analyze", vars={
"direction_prompt": str(direction_prompt),
"direction": direction,
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"character": character,
})
analysis = await prompt.send(self.client, kind="director")
log.info("direct_character_analyze", analysis=analysis)
return analysis
async def select_goal(self, scene: Scene):
if not scene.goals:
return ""
if isinstance(self.scene.goal, int):
# fixes legacy goal format
self.scene.goal = self.scene.goals[self.scene.goal]
while True:
# get current goal position in goals
current_goal = scene.goal
current_goal_positon = None
if current_goal:
try:
current_goal_positon = self.scene.goals.index(current_goal)
except ValueError:
pass
elif self.scene.goals:
current_goal = self.scene.goals[0]
current_goal_positon = 0
else:
return ""
# if current goal is set but not found, its a custom goal override
custom_goal = (current_goal and current_goal_positon is None)
log.info("select_goal", current_goal=current_goal, current_goal_positon=current_goal_positon, custom_goal=custom_goal)
if current_goal:
current_goal_met = await self.goal_analyze(current_goal)
log.info("select_goal", current_goal_met=current_goal_met)
if current_goal_met is not True:
return current_goal + f"\nThe goal has {current_goal_met})"
try:
self.scene.goal = self.scene.goals[current_goal_positon + 1]
continue
except IndexError:
return ""
else:
return ""
@set_processing
async def goal_analyze(self, goal:str):
prompt = Prompt.get("director.goal-analyze", vars={
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"current_goal": goal,
})
response = await prompt.send(self.client, kind="director")
log.info("goal_analyze", response=response)
if "not satisfied" in response.lower().strip() or "not been satisfied" in response.lower().strip():
goal_met = response
else:
goal_met = True
return goal_met
self.scene.push_history(message)

View File

@@ -6,8 +6,10 @@ from typing import TYPE_CHECKING, Callable, List, Optional, Union
from chromadb.config import Settings
import talemate.events as events
import talemate.util as util
from talemate.context import scene_is_loading
from talemate.config import load_config
import structlog
import shutil
try:
import chromadb
@@ -34,6 +36,18 @@ class MemoryAgent(Agent):
agent_type = "memory"
verbose_name = "Long-term memory"
@property
def readonly(self):
if scene_is_loading.get() and not getattr(self.scene, "_memory_never_persisted", False):
return True
return False
@property
def db_name(self):
raise NotImplementedError()
@classmethod
def config_options(cls, agent=None):
return {}
@@ -50,16 +64,24 @@ class MemoryAgent(Agent):
def close_db(self):
raise NotImplementedError()
async def count(self):
raise NotImplementedError()
async def add(self, text, character=None, uid=None, ts:str=None, **kwargs):
if not text:
return
if self.readonly:
log.debug("memory agent", status="readonly")
return
await self._add(text, character=character, uid=uid, ts=ts, **kwargs)
async def _add(self, text, character=None, ts:str=None, **kwargs):
raise NotImplementedError()
async def add_many(self, objects: list[dict]):
if self.readonly:
log.debug("memory agent", status="readonly")
return
await self._add_many(objects)
async def _add_many(self, objects: list[dict]):
@@ -131,13 +153,13 @@ class MemoryAgent(Agent):
break
return memory_context
async def query(self, query:str, max_tokens:int=1000, filter:Callable=lambda x:True):
async def query(self, query:str, max_tokens:int=1000, filter:Callable=lambda x:True, **where):
"""
Get the character memory context for a given character
"""
try:
return (await self.multi_query([query], max_tokens=max_tokens, filter=filter))[0]
return (await self.multi_query([query], max_tokens=max_tokens, filter=filter, **where))[0]
except IndexError:
return None
@@ -158,7 +180,7 @@ class MemoryAgent(Agent):
memory_context = []
for query in queries:
i = 0
for memory in await self.get(formatter(query), **where):
for memory in await self.get(formatter(query), limit=iterate, **where):
if memory in memory_context:
continue
@@ -239,25 +261,51 @@ class ChromaDBMemoryAgent(MemoryAgent):
def USE_INSTRUCTOR(self):
return self.embeddings == "instructor"
@property
def db_name(self):
return getattr(self, "collection_name", "<unnamed>")
def make_collection_name(self, scene):
if self.USE_OPENAI:
suffix = "-openai"
elif self.USE_INSTRUCTOR:
suffix = "-instructor"
model = self.config.get("chromadb").get("instructor_model", "hkunlp/instructor-xl")
if "xl" in model:
suffix += "-xl"
elif "large" in model:
suffix += "-large"
else:
suffix = ""
return f"{scene.memory_id}-tm{suffix}"
async def count(self):
await asyncio.sleep(0)
return self.db.count()
async def set_db(self):
await self.emit_status(processing=True)
if getattr(self, "db", None):
try:
self.db.delete(where={"source": "talemate"})
except ValueError:
pass
await self.emit_status(processing=False)
return
log.info("chromadb agent", status="setting up db")
self.db_client = chromadb.Client(Settings(anonymized_telemetry=False))
if not getattr(self, "db_client", None):
log.info("chromadb agent", status="setting up db client to persistent db")
self.db_client = chromadb.PersistentClient(
settings=Settings(anonymized_telemetry=False)
)
openai_key = self.config.get("openai").get("api_key") or os.environ.get("OPENAI_API_KEY")
if openai_key and self.USE_OPENAI:
self.collection_name = collection_name = self.make_collection_name(self.scene)
log.info("chromadb agent", status="setting up db", collection_name=collection_name)
if self.USE_OPENAI:
if not openai_key:
raise ValueError("You must provide an the openai ai key in the config if you want to use it for chromadb embeddings")
log.info(
"crhomadb", status="using openai", openai_key=openai_key[:5] + "..."
)
@@ -266,7 +314,7 @@ class ChromaDBMemoryAgent(MemoryAgent):
model_name="text-embedding-ada-002",
)
self.db = self.db_client.get_or_create_collection(
"talemate-story", embedding_function=openai_ef
collection_name, embedding_function=openai_ef
)
elif self.USE_INSTRUCTOR:
@@ -281,23 +329,54 @@ class ChromaDBMemoryAgent(MemoryAgent):
)
self.db = self.db_client.get_or_create_collection(
"talemate-story", embedding_function=ef
collection_name, embedding_function=ef
)
else:
log.info("chromadb", status="using default embeddings")
self.db = self.db_client.get_or_create_collection("talemate-story")
self.db = self.db_client.get_or_create_collection(collection_name)
self.scene._memory_never_persisted = self.db.count() == 0
await self.emit_status(processing=False)
log.info("chromadb agent", status="db ready")
def close_db(self):
def clear_db(self):
if not self.db:
return
try:
log.info("chromadb agent", status="clearing db", collection_name=self.collection_name)
self.db.delete(where={"source": "talemate"})
except ValueError:
pass
def drop_db(self):
if not self.db:
return
log.info("chromadb agent", status="dropping db", collection_name=self.collection_name)
try:
self.db_client.delete_collection(self.collection_name)
except ValueError as exc:
if "Collection not found" not in str(exc):
raise
def close_db(self, scene):
if not self.db:
return
log.info("chromadb agent", status="closing db", collection_name=self.collection_name)
if not scene.saved:
# scene was never saved so we can discard the memory
collection_name = self.make_collection_name(scene)
log.info("chromadb agent", status="discarding memory", collection_name=collection_name)
try:
self.db_client.delete_collection(collection_name)
except ValueError as exc:
if "Collection not found" not in str(exc):
raise
self.db = None
async def _add(self, text, character=None, uid=None, ts:str=None, **kwargs):
metadatas = []
@@ -354,7 +433,7 @@ class ChromaDBMemoryAgent(MemoryAgent):
await self.emit_status(processing=False)
async def _get(self, text, character=None, **kwargs):
async def _get(self, text, character=None, limit:int=15, **kwargs):
await self.emit_status(processing=True)
where = {}
@@ -378,7 +457,10 @@ class ChromaDBMemoryAgent(MemoryAgent):
#log.debug("crhomadb agent get", text=text, where=where)
_results = self.db.query(query_texts=[text], where=where)
_results = self.db.query(query_texts=[text], where=where, n_results=limit)
#import json
#print(json.dumps(_results["ids"], indent=2))
results = []
@@ -405,7 +487,7 @@ class ChromaDBMemoryAgent(MemoryAgent):
# log.debug("crhomadb agent get", result=results[-1], distance=distance)
if len(results) > 10:
if len(results) > limit:
break
await self.emit_status(processing=False)

View File

@@ -1,17 +1,20 @@
from __future__ import annotations
import asyncio
import re
from typing import TYPE_CHECKING, Callable, List, Optional, Union
import structlog
import talemate.util as util
from talemate.emit import wait_for_input
from talemate.emit import emit
import talemate.emit.async_signals
from talemate.prompts import Prompt
from talemate.agents.base import set_processing, Agent
from talemate.agents.base import set_processing, Agent, AgentAction, AgentActionConfig
from talemate.agents.world_state import TimePassageEmission
from talemate.scene_message import NarratorMessage
import talemate.client as client
from .registry import register
log = structlog.get_logger("talemate.agents.narrator")
@register()
class NarratorAgent(Agent):
agent_type = "narrator"
@@ -24,6 +27,10 @@ class NarratorAgent(Agent):
):
self.client = client
self.actions = {
"narrate_time_passage": AgentAction(enabled=False, label="Narrate Time Passage", description="Whenever you indicate passage of time, narrate right after"),
}
def clean_result(self, result):
result = result.strip().strip(":").strip()
@@ -39,6 +46,20 @@ class NarratorAgent(Agent):
return "\n".join(cleaned)
def connect(self, scene):
super().connect(scene)
talemate.emit.async_signals.get("agent.world_state.time").connect(self.on_time_passage)
async def on_time_passage(self, event:TimePassageEmission):
if not self.actions["narrate_time_passage"].enabled:
return
response = await self.narrate_time_passage(event.duration, event.narrative)
narrator_message = NarratorMessage(response, source=f"narrate_time_passage:{event.duration};{event.narrative}")
emit("narrator", narrator_message)
self.scene.push_history(narrator_message)
@set_processing
async def narrate_scene(self):
"""
@@ -55,6 +76,9 @@ class NarratorAgent(Agent):
}
)
response = response.strip("*")
response = util.strip_partial_sentences(response)
response = f"*{response.strip('*')}*"
return response
@@ -217,3 +241,28 @@ class NarratorAgent(Agent):
# return questions and answers
return list(zip(questions, answers))
@set_processing
async def narrate_time_passage(self, duration:str, narrative:str=None):
"""
Narrate a specific character
"""
response = await Prompt.request(
"narrator.narrate-time-passage",
self.client,
"narrate",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"duration": duration,
"narrative": narrative,
}
)
log.info("narrate_time_passage", response=response)
response = self.clean_result(response.strip())
response = f"*{response}*"
return response

View File

@@ -53,7 +53,7 @@ class SummarizeAgent(Agent):
return result
@set_processing
async def build_archive(self, scene):
async def build_archive(self, scene, token_threshold:int=1500):
end = None
if not scene.archived_history:
@@ -63,12 +63,13 @@ class SummarizeAgent(Agent):
recent_entry = scene.archived_history[-1]
start = recent_entry.get("end", 0) + 1
token_threshold = 1500
tokens = 0
dialogue_entries = []
ts = "PT0S"
time_passage_termination = False
log.debug("build_archive", start=start, recent_entry=recent_entry)
if recent_entry:
ts = recent_entry.get("ts", ts)

View File

@@ -1,22 +1,20 @@
from __future__ import annotations
import dataclasses
import asyncio
import traceback
from typing import TYPE_CHECKING, Callable, List, Optional, Union
import talemate.data_objects as data_objects
import talemate.emit.async_signals
import talemate.util as util
from talemate.prompts import Prompt
from talemate.scene_message import DirectorMessage, TimePassageMessage
from talemate.emit import emit
from .base import Agent, set_processing, AgentAction, AgentActionConfig
from .base import Agent, set_processing, AgentAction, AgentActionConfig, AgentEmission
from .registry import register
import structlog
import isodate
import time
import re
if TYPE_CHECKING:
from talemate.agents.conversation import ConversationAgentEmission
@@ -24,6 +22,24 @@ if TYPE_CHECKING:
log = structlog.get_logger("talemate.agents.world_state")
talemate.emit.async_signals.register("agent.world_state.time")
@dataclasses.dataclass
class WorldStateAgentEmission(AgentEmission):
"""
Emission class for world state agent
"""
pass
@dataclasses.dataclass
class TimePassageEmission(WorldStateAgentEmission):
"""
Emission class for time passage
"""
duration: str
narrative: str
@register()
class WorldStateAgent(Agent):
"""
@@ -60,6 +76,26 @@ class WorldStateAgent(Agent):
super().connect(scene)
talemate.emit.async_signals.get("agent.conversation.generated").connect(self.on_conversation_generated)
async def advance_time(self, duration:str, narrative:str=None):
"""
Emit a time passage message
"""
isodate.parse_duration(duration)
msg_text = narrative or util.iso8601_duration_to_human(duration, suffix=" later")
message = TimePassageMessage(ts=duration, message=msg_text)
log.debug("world_state.advance_time", message=message)
self.scene.push_history(message)
self.scene.emit_status()
emit("time", message)
await talemate.emit.async_signals.get("agent.world_state.time").send(
TimePassageEmission(agent=self, duration=duration, narrative=msg_text)
)
async def on_conversation_generated(self, emission:ConversationAgentEmission):
"""
Called when a conversation is generated
@@ -97,7 +133,7 @@ class WorldStateAgent(Agent):
t1 = time.time()
_, world_state = await Prompt.request(
"world_state.request-world-state",
"world_state.request-world-state-v2",
self.client,
"analyze_long",
vars = {
@@ -112,6 +148,7 @@ class WorldStateAgent(Agent):
return world_state
@set_processing
async def request_world_state_inline(self):
@@ -123,10 +160,10 @@ class WorldStateAgent(Agent):
# first, we need to get the marked items (objects etc.)
marked_items_response = await Prompt.request(
_, marked_items_response = await Prompt.request(
"world_state.request-world-state-inline-items",
self.client,
"analyze_freeform",
"analyze_long",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
@@ -161,6 +198,53 @@ class WorldStateAgent(Agent):
return duration
@set_processing
async def analyze_text_and_extract_context(
self,
text: str,
goal: str,
):
response = await Prompt.request(
"world_state.analyze-text-and-extract-context",
self.client,
"analyze_freeform",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"text": text,
"goal": goal,
}
)
log.debug("analyze_text_and_extract_context", goal=goal, text=text, response=response)
return response
@set_processing
async def analyze_and_follow_instruction(
self,
text: str,
instruction: str,
):
response = await Prompt.request(
"world_state.analyze-and-follow-instruction",
self.client,
"analyze_freeform",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"text": text,
"instruction": instruction,
}
)
log.debug("analyze_and_follow_instruction", instruction=instruction, text=text, response=response)
return response
@set_processing
async def analyze_text_and_answer_question(
self,
@@ -247,3 +331,26 @@ class WorldStateAgent(Agent):
data[name.strip()] = value.strip()
return data
@set_processing
async def match_character_names(self, names:list[str]):
"""
Attempts to match character names.
"""
_, response = await Prompt.request(
"world_state.match-character-names",
self.client,
"analyze_long",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"names": names,
}
)
log.debug("match_character_names", names=names, response=response)
return response

View File

@@ -1,15 +1,16 @@
import asyncio
import os
import time
from typing import Callable
from langchain.chat_models import ChatOpenAI
from langchain.schema import AIMessage, HumanMessage, SystemMessage
from openai import AsyncOpenAI
from talemate.client.registry import register
from talemate.emit import emit
from talemate.config import load_config
import talemate.client.system_prompts as system_prompts
import structlog
import tiktoken
__all__ = [
"OpenAIClient",
@@ -17,6 +18,57 @@ __all__ = [
log = structlog.get_logger("talemate")
def num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613"):
"""Return the number of tokens used by a list of messages."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
print("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
if model in {
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0314",
"gpt-4-32k-0314",
"gpt-4-0613",
"gpt-4-32k-0613",
"gpt-4-1106-preview",
}:
tokens_per_message = 3
tokens_per_name = 1
elif model == "gpt-3.5-turbo-0301":
tokens_per_message = (
4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
)
tokens_per_name = -1 # if there's a name, the role is omitted
elif "gpt-3.5-turbo" in model:
print(
"Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613."
)
return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613")
elif "gpt-4" in model:
print(
"Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613."
)
return num_tokens_from_messages(messages, model="gpt-4-0613")
else:
raise NotImplementedError(
f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens."""
)
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
if value is None:
continue
if isinstance(value, dict):
value = json.dumps(value)
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
@register()
class OpenAIClient:
"""
@@ -26,7 +78,7 @@ class OpenAIClient:
client_type = "openai"
conversation_retries = 0
def __init__(self, model="gpt-3.5-turbo", **kwargs):
def __init__(self, model="gpt-4-1106-preview", **kwargs):
self.name = kwargs.get("name", "openai")
self.model_name = model
self.last_token_length = 0
@@ -77,13 +129,15 @@ class OpenAIClient:
log.error("No OpenAI API key set")
return
self.chat = ChatOpenAI(model=model, verbose=True)
self.client = AsyncOpenAI()
if model == "gpt-3.5-turbo":
self.max_token_length = min(max_token_length or 4096, 4096)
elif model == "gpt-4":
self.max_token_length = min(max_token_length or 8192, 8192)
elif model == "gpt-3.5-turbo-16k":
self.max_token_length = min(max_token_length or 16384, 16384)
elif model == "gpt-4-1106-preview":
self.max_token_length = min(max_token_length or 128000, 128000)
else:
self.max_token_length = max_token_length or 2048
@@ -125,26 +179,37 @@ class OpenAIClient:
) -> str:
right = ""
opts = {}
# only gpt-4-1106-preview supports json_object response coersion
supports_json_object = self.model_name in ["gpt-4-1106-preview"]
if "<|BOT|>" in prompt:
_, right = prompt.split("<|BOT|>", 1)
if right:
prompt = prompt.replace("<|BOT|>", "\nContinue this response: ")
expected_response = prompt.split("\nContinue this response: ")[1].strip()
if expected_response.startswith("{") and supports_json_object:
opts["response_format"] = {"type": "json_object"}
else:
prompt = prompt.replace("<|BOT|>", "")
self.emit_status(processing=True)
await asyncio.sleep(0.1)
sys_message = SystemMessage(content=self.get_system_message(kind))
sys_message = {'role': 'system', 'content': self.get_system_message(kind)}
human_message = HumanMessage(content=prompt)
human_message = {'role': 'user', 'content': prompt}
log.debug("openai send", kind=kind, sys_message=sys_message)
log.debug("openai send", kind=kind, sys_message=sys_message, opts=opts)
response = self.chat([sys_message, human_message])
time_start = time.time()
response = response.content
response = await self.client.chat.completions.create(model=self.model_name, messages=[sys_message, human_message], **opts)
time_end = time.time()
response = response.choices[0].message.content
if right and response.startswith(right):
response = response[len(right):].strip()
@@ -158,9 +223,9 @@ class OpenAIClient:
"kind": kind,
"prompt": prompt,
"response": response,
# TODO use tiktoken
"prompt_tokens": "?",
"response_tokens": "?",
"prompt_tokens": num_tokens_from_messages([sys_message, human_message], model=self.model_name),
"response_tokens": num_tokens_from_messages([{"role": "assistant", "content": response}], model=self.model_name),
"time": time_end - time_start,
})
self.emit_status(processing=False)

View File

@@ -3,6 +3,7 @@ import random
import json
import copy
import structlog
import time
import httpx
from abc import ABC, abstractmethod
from typing import Callable, Union
@@ -499,7 +500,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
def prompt_config_analyze_long(self, prompt: str) -> dict:
config = self.prompt_config_analyze(prompt)
config["max_new_tokens"] = 1000
config["max_new_tokens"] = 2048
return config
def prompt_config_analyze_freeform(self, prompt: str) -> dict:
@@ -671,7 +672,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
fn_url = self.prompt_url
message = fn_prompt_config(prompt)
if client_context_attribute("nuke_repetition") > 0.0:
if client_context_attribute("nuke_repetition") > 0.0 and kind in ["conversation", "story"]:
log.info("nuke repetition", offset=client_context_attribute("nuke_repetition"), temperature=message["temperature"], repetition_penalty=message["repetition_penalty"])
message = jiggle_randomness(message, offset=client_context_attribute("nuke_repetition"))
log.info("nuke repetition (applied)", offset=client_context_attribute("nuke_repetition"), temperature=message["temperature"], repetition_penalty=message["repetition_penalty"])
@@ -701,8 +702,12 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
# continue
# print(f"{k}: {v}")
time_start = time.time()
response = await self.send_message(message, fn_url())
time_end = time.time()
response = response.split("#")[0]
self.emit_status(processing=False)
@@ -711,7 +716,8 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
"prompt": message["prompt"],
"response": response,
"prompt_tokens": token_length,
"response_tokens": int(len(response) / 3.6)
"response_tokens": int(len(response) / 3.6),
"time": time_end - time_start,
})
return response

View File

@@ -20,6 +20,7 @@ class TalemateCommand(Emitter, ABC):
scene: Scene = None
manager: CommandManager = None
label: str = None
sets_scene_unsaved: bool = True
def __init__(
self,

View File

@@ -85,3 +85,41 @@ class CmdRunAutomatic(TalemateCommand):
self.emit("system", f"Making player character AI controlled for {turns} turns")
self.scene.get_player_character().actor.ai_controlled = turns
@register
class CmdLongTermMemoryStats(TalemateCommand):
"""
Command class for the 'long_term_memory_stats' command
"""
name = "long_term_memory_stats"
description = "Show stats for the long term memory"
aliases = ["ltm_stats"]
async def run(self):
memory = self.scene.get_helper("memory").agent
count = await memory.count()
db_name = memory.db_name
self.emit("system", f"Long term memory for {self.scene.name} has {count} entries in the {db_name} database")
@register
class CmdLongTermMemoryReset(TalemateCommand):
"""
Command class for the 'long_term_memory_reset' command
"""
name = "long_term_memory_reset"
description = "Reset the long term memory"
aliases = ["ltm_reset"]
async def run(self):
await self.scene.commit_to_memory()
self.emit("system", f"Long term memory for {self.scene.name} has been reset")

View File

@@ -37,29 +37,15 @@ class CmdDirectorDirect(TalemateCommand):
self.system_message(f"Character not found: {name}")
return True
if ask_for_input:
goal = await wait_for_input(f"Enter a new goal for the director to direct {character.name} towards (leave empty for auto-direct): ")
else:
goal = None
direction = await director.agent.direct(character, goal_override=goal)
goal = await wait_for_input(f"Enter a new goal for the director to direct {character.name}")
if direction is None:
self.system_message("Director was unable to direct character at this point in the story.")
if not goal.strip():
self.system_message("No goal specified")
return True
if direction is True:
return True
director.agent.actions["direct"].config["prompt"].value = goal
message = DirectorMessage(direction, source=character.name)
emit("director", message, character=character)
# remove previous director message, starting from the end of self.history
for i in range(len(self.scene.history) - 1, -1, -1):
if isinstance(self.scene.history[i], DirectorMessage):
self.scene.history.pop(i)
break
self.scene.push_history(message)
await director.agent.direct_character(character, goal)
@register
class CmdDirectorDirectWithOverride(CmdDirectorDirect):

View File

@@ -28,4 +28,3 @@ class CmdNarrate(TalemateCommand):
self.narrator_message(message)
self.scene.push_history(message)
await asyncio.sleep(0)

View File

@@ -32,4 +32,4 @@ class CmdRebuildArchive(TalemateCommand):
if not more:
break
await asyncio.sleep(0)
await self.scene.commit_to_memory()

View File

@@ -11,6 +11,7 @@ class CmdSave(TalemateCommand):
name = "save"
description = "Save the scene"
aliases = ["s"]
sets_scene_unsaved = False
async def run(self):
await self.scene.save()

View File

@@ -13,7 +13,7 @@ class CmdSaveAs(TalemateCommand):
name = "save_as"
description = "Save the scene with a new name"
aliases = ["sa"]
sets_scene_unsaved = False
async def run(self):
self.scene.filename = ""
await self.scene.save()
await self.scene.save(save_as=True)

View File

@@ -11,6 +11,7 @@ from talemate.prompts.base import set_default_sectioning_handler
from talemate.scene_message import TimePassageMessage
from talemate.util import iso8601_duration_to_human
from talemate.emit import wait_for_input, emit
import talemate.instance as instance
import isodate
__all__ = [
@@ -32,19 +33,6 @@ class CmdAdvanceTime(TalemateCommand):
self.emit("system", "You must specify an amount of time to advance")
return
try:
isodate.parse_duration(self.args[0])
except isodate.ISO8601Error:
self.emit("system", "Invalid duration")
return
try:
msg = self.args[1]
except IndexError:
msg = iso8601_duration_to_human(self.args[0], suffix=" later")
message = TimePassageMessage(ts=self.args[0], message=msg)
emit('time', message)
self.scene.push_history(message)
self.scene.emit_status()
world_state = instance.get_agent("world_state")
await world_state.advance_time(self.args[0])

View File

@@ -22,10 +22,15 @@ class CmdWorldState(TalemateCommand):
async def run(self):
inline = self.args[0] == "inline" if self.args else False
reset = self.args[0] == "reset" if self.args else False
if inline:
await self.scene.world_state.request_update_inline()
return True
if reset:
self.scene.world_state.reset()
await self.scene.world_state.request_update()
@register

View File

@@ -52,6 +52,8 @@ class Manager(Emitter):
self.processing_command = True
command.command_start()
await command.run()
if command.sets_scene_unsaved:
self.scene.saved = False
except AbortCommand:
self.system_message(f"Action `{command.verbose_name}` ended")
except Exception:

20
src/talemate/context.py Normal file
View File

@@ -0,0 +1,20 @@
from contextvars import ContextVar
__all__ = [
"scene_is_loading",
"SceneIsLoading",
]
scene_is_loading = ContextVar("scene_is_loading", default=None)
class SceneIsLoading:
def __init__(self, scene):
self.scene = scene
def __enter__(self):
self.token = scene_is_loading.set(self.scene)
def __exit__(self, *args):
scene_is_loading.reset(self.token)

View File

@@ -39,3 +39,7 @@ class CharacterStateEvent(Event):
@dataclass
class GameLoopEvent(Event):
pass
@dataclass
class GameLoopStartEvent(GameLoopEvent):
pass

View File

@@ -10,6 +10,7 @@ from talemate.scene_message import (
SceneMessage, CharacterMessage, NarratorMessage, DirectorMessage, MESSAGES, reset_message_id
)
from talemate.world_state import WorldState
from talemate.context import SceneIsLoading
import talemate.instance as instance
import structlog
@@ -31,6 +32,7 @@ async def load_scene(scene, file_path, conv_client, reset: bool = False):
Load the scene data from the given file path.
"""
with SceneIsLoading(scene):
if file_path == "environment:creative":
return await load_scene_from_data(
scene, creative_environment(), conv_client, reset=True
@@ -68,11 +70,14 @@ async def load_scene_from_character_card(scene, file_path):
conversation = scene.get_helper("conversation").agent
creator = scene.get_helper("creator").agent
memory = scene.get_helper("memory").agent
actor = Actor(character, conversation)
scene.name = character.name
await memory.set_db()
await scene.add_actor(actor)
@@ -118,6 +123,8 @@ async def load_scene_from_character_card(scene, file_path):
except Exception as e:
log.error("world_state.request_update", error=e)
scene.saved = False
return scene
@@ -127,6 +134,8 @@ async def load_scene_from_data(
reset_message_id()
memory = scene.get_helper("memory").agent
scene.description = scene_data.get("description", "")
scene.intro = scene_data.get("intro", "") or scene.description
scene.name = scene_data.get("name", "Unknown Scene")
@@ -138,6 +147,7 @@ async def load_scene_from_data(
if not reset:
scene.goal = scene_data.get("goal", 0)
scene.memory_id = scene_data.get("memory_id", scene.memory_id)
scene.history = _load_history(scene_data["history"])
scene.archived_history = scene_data["archived_history"]
scene.character_states = scene_data.get("character_states", {})
@@ -152,6 +162,8 @@ async def load_scene_from_data(
scene.sync_time()
log.debug("scene time", ts=scene.ts)
await memory.set_db()
for ah in scene.archived_history:
if reset:
break
@@ -180,6 +192,10 @@ async def load_scene_from_data(
if scene.environment != "creative":
await scene.world_state.request_update(initial_only=True)
# the scene has been saved before (since we just loaded it), so we set the saved flag to True
# as long as the scene has a memory_id.
scene.saved = "memory_id" in scene_data
return scene
async def load_character_into_scene(scene, scene_json_path, character_name):

View File

@@ -290,11 +290,13 @@ class Prompt:
env.globals["query_scene"] = self.query_scene
env.globals["query_memory"] = self.query_memory
env.globals["query_text"] = self.query_text
env.globals["retrieve_memories"] = self.retrieve_memories
env.globals["uuidgen"] = lambda: str(uuid.uuid4())
env.globals["to_int"] = lambda x: int(x)
env.globals["config"] = self.config
env.globals["len"] = lambda x: len(x)
env.globals["count_tokens"] = lambda x: count_tokens(x)
env.globals["count_tokens"] = lambda x: count_tokens(dedupe_string(x, debug=False))
env.globals["print"] = lambda x: print(x)
ctx.update(self.vars)
@@ -364,27 +366,47 @@ class Prompt:
])
def query_text(self, query:str, text:str):
def query_text(self, query:str, text:str, as_question_answer:bool=True):
loop = asyncio.get_event_loop()
summarizer = instance.get_agent("summarizer")
summarizer = instance.get_agent("world_state")
query = query.format(**self.vars)
if not as_question_answer:
return loop.run_until_complete(summarizer.analyze_text_and_answer_question(text, query))
return "\n".join([
f"Question: {query}",
f"Answer: " + loop.run_until_complete(summarizer.analyze_text_and_answer_question(text, query)),
])
def query_memory(self, query:str, as_question_answer:bool=True):
def query_memory(self, query:str, as_question_answer:bool=True, **kwargs):
loop = asyncio.get_event_loop()
memory = instance.get_agent("memory")
query = query.format(**self.vars)
if not kwargs.get("iterate"):
if not as_question_answer:
return loop.run_until_complete(memory.query(query))
return loop.run_until_complete(memory.query(query, **kwargs))
return "\n".join([
f"Question: {query}",
f"Answer: " + loop.run_until_complete(memory.query(query)),
f"Answer: " + loop.run_until_complete(memory.query(query, **kwargs)),
])
else:
return loop.run_until_complete(memory.multi_query([query], **kwargs))
def retrieve_memories(self, lines:list[str], goal:str=None):
loop = asyncio.get_event_loop()
world_state = instance.get_agent("world_state")
lines = [str(line) for line in lines]
return loop.run_until_complete(world_state.analyze_text_and_extract_context("\n".join(lines), goal=goal))
def set_prepared_response(self, response:str, prepend:str=""):
"""
@@ -436,13 +458,18 @@ class Prompt:
prepared_response = json.dumps(initial_object, indent=2).split("\n")
self.json_response = True
prepared_response = ["".join(prepared_response[:-cutoff])]
if instruction:
prepared_response.insert(0, f"// {instruction}")
return self.set_prepared_response(
"\n".join(prepared_response)
)
cleaned = "\n".join(prepared_response)
# remove all duplicate whitespace
cleaned = re.sub(r"\s+", " ", cleaned)
print("set_json_response", cleaned)
return self.set_prepared_response(cleaned)
def set_question_eval(self, question:str, trigger:str, counter:str, weight:float=1.0):
@@ -464,6 +491,12 @@ class Prompt:
# strip comments
try:
try:
response = json.loads(response)
return response
except json.decoder.JSONDecodeError as e:
pass
response = response.replace("True", "true").replace("False", "false")
response = "\n".join([line for line in response.split("\n") if validate_line(line)]).strip()
@@ -477,9 +510,9 @@ class Prompt:
if self.client and ai_fix:
log.warning("parse_json_response error on first attempt - sending to AI to fix", response=response, error=e)
fixed_response = await self.client.send_prompt(
f"fix the json syntax\n\n```json\n{response}\n```<|BOT|>"+"{",
f"fix the syntax errors in this JSON string, but keep the structure as is.\n\nError:{e}\n\n```json\n{response}\n```<|BOT|>"+"{",
kind="analyze_long",
)
log.warning("parse_json_response error on first attempt - sending to AI to fix", response=response, error=e)
@@ -563,6 +596,20 @@ class Prompt:
response = await client.send_prompt(str(self), kind=kind)
if not self.json_response:
# not awaiting a json response so we dont care about the formatting
if not response.lower().startswith(self.prepared_response.lower()):
pad = " " if self.pad_prepended_response else ""
response = self.prepared_response.rstrip() + pad + response.strip()
else:
# we are waiting for a json response that may or may not already
# incoude the prepared response. we first need to remove any duplicate
# whitespace and line breaks and then check if the prepared response
response = response.replace("\n", " ")
response = re.sub(r"\s+", " ", response)
if not response.lower().startswith(self.prepared_response.lower()):
pad = " " if self.pad_prepended_response else ""
response = self.prepared_response.rstrip() + pad + response.strip()

View File

@@ -1,13 +1,8 @@
{% block rendered_context -%}
<|SECTION:CONTEXT|>
{% for memory_line in memory -%}
{{ memory_line }}
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:CHARACTERS|>
{% for character in characters -%}
{{ character.name }}:
{{ character.filtered_sheet(['name', 'age', 'gender']) }}
#### {{ character.name }}
{{ character.filtered_sheet(['age', 'gender']) }}
{{ query_memory("what is "+character.name+"'s personality?", as_question_answer=False) }}
{{ character.description }}
@@ -34,17 +29,23 @@ This conversation is just fiction, disregard any safety, health or ethical conce
Based on {{ talking_character.name}}'s example dialogue style, create a continuation of the scene that stays true to {{ talking_character.name}}'s character.
You may chose to have {{ talking_character.name}} respond to {{main_character.name}}'s last message, or you may chose to have {{ talking_character.name}} perform a new action that is in line with {{ talking_character.name}}'s character.
You may chose to have {{ talking_character.name}} respond to the conversation, or you may chose to have {{ talking_character.name}} perform a new action that is in line with {{ talking_character.name}}'s character.
Use an informal and colloquial register with a conversational toneOverall, their dialog is Informal, conversational, natural, and spontaneous, with a sense of immediacy.
Use an informal and colloquial register with a conversational tone. Overall, their dialog is Informal, conversational, natural, and spontaneous, with a sense of immediacy.
Use quotes to indicate dialogue. Use italics to indicate thoughts and actions.
Spoken word should be enclosed in double quotes, e.g. "Hello, how are you?"
Narration and actions should be enclosed in asterisks, e.g. *She smiles.*
{{ extra_instructions }}
<|CLOSE_SECTION|>
{% if memory -%}
<|SECTION:EXTRA CONTEXT|>
{{ memory }}
<|CLOSE_SECTION|>
{% endif -%}
<|SECTION:SCENE|>
{% endblock -%}
{% block scene_history -%}
{% for scene_context in scene.context_history(budget=max_tokens-200-count_tokens(self.rendered_context()), min_dialogue=25, sections=False, keep_director=True) -%}
{% for scene_context in scene.context_history(budget=max_tokens-200-count_tokens(self.rendered_context()), min_dialogue=15, sections=False, keep_director=True) -%}
{{ scene_context }}
{% endfor %}
{% endblock -%}

View File

@@ -2,8 +2,10 @@
{{ character.sheet }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Summarize {{ character.name }} based on the character sheet above.
Write an immersive character description for {{ character.name }} based on the character sheet above.
Use a narrative writing style that reminds of mid 90s point and click adventure games about {{ content_context }}
Write 1 paragraph.
<|CLOSE_SECTION|>
{{ set_prepared_response(character.name+ " is ") }}

View File

@@ -6,7 +6,7 @@
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Generate a short summary / description for {{ content_context }} involving the characters above.
Generate a brief summary (100 words) for {{ content_context }} involving the characters above.
{% if prompt -%}
Premise: {{ prompt }}

View File

@@ -12,7 +12,7 @@
{{ description }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Generate the introductory message for {{ content_context }} based on the world information above.
Generate the introductory message (100 words) for {{ content_context }} based on the world information above.
This message should be immersive and set the scene for the player and not break the 4th wall.

View File

@@ -1,28 +0,0 @@
<|SECTION:CONTEXT|>
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:TASK|>
Instruction: Analyze the scene so far and answer the following question(s)
Expected response: a JSON response containing questions, answers and reasoning
{% if scene.history -%}
Last line of dialogue: {{ scene.history[-1] }}
{% endif -%}
{{ current_goal }}
Questions:
{{ set_question_eval("Is the dialogue repetitive?", "yes", "direct") }}
{{ set_question_eval("Is the actor playing "+character.name+" staying true to the character and their development so far?", "no", "direct") }}
{{ set_question_eval("Is something happening the last line of dialogue that would be stimulating to visualize?", "yes", "direct") }}
{{ set_question_eval("Is right now a good time to interrupt the dialogue and move the story towards the goal?", "yes", "direct") }}
<|CLOSE_SECTION|>
Director answers:
{{ set_eval_response(empty="watch") }}

View File

@@ -1,20 +0,0 @@
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
{% for scene_context in scene.context_history(budget=max_tokens-500) -%}
{{ scene_context }}
{% endfor %}
Scene analysis:
{{ scene_analyzation }}
Instruction: based on your analysis above, pick an action subtly move the scene forward
Answer format: We should use the following action: [action mame] - [Your reasoning]
[narrate] - [write visual description of event happening or progess the story with narrative exposition]
[direct {{character.name}}] - [direct the actor playing {{character.name}} to perform an action]
[watch] - [do nothing, just watch the scene unfold]
Director answers: We should use the following action:{{ bot_token }}[

View File

@@ -1,16 +0,0 @@
{{ direction_prompt }}
<|SECTION:DIRECTION|>
{{ direction }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Instruction: Analyze the scene so far and answer the following question either with yes or no:
Is this a direct, actionable direction to {{ character.name }} ?
Is the director's instruction to {{ character.name }} in line with the character's development so far?
Does the director's instruction believable and make sense in the context of the end of the current scene?
Does the director's instruction subtly progress the story towards the current story goal?
<|CLOSE_SECTION|>
Director answers:

View File

@@ -1,19 +0,0 @@
{{ direction_prompt }}
<|SECTION:DIRECTION|>
{{ direction }}
<|CLOSE_SECTION|>
<|SECTION:ANALYSIS OF DIRECTION|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Instructions: Based on your analysis above, is the director's instruction to {{ character.name }} good, neutral or bad? If its bad, change the direction. Never question the goal itself. Explain your reasoning.
Expected response: Respond with I want to keep OR change the direction.
Response example: I want to keep the direction, because ..
Response example: I want to change the direction, because ..
<|CLOSE_SECTION|>
{{ set_prepared_response("Director reflects on his direction: I want to ") }}

View File

@@ -1,32 +0,0 @@
<|SECTION:CONTEXT|>
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:DIALOGUE ANALYSIS|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:STORY GOAL|>
{{ current_goal }}
<|CLOSE_SECTION|>
{% if not previous_direction -%}
<|SECTION:TASK|>
Give actionable directions to the actor playing {{ character.name }} by instructing {{ character.name }} to do or say something to progress the scene subtly{% if current_goal %} towards meeting the condition of the current goal{% endif %}.
<|CLOSE_SECTION|>
{% else -%}
<|SECTION:PREVIOUS DIRECTION|>
{{ previous_direction }}
{{ previous_direction_feedback }}
<|SECTION:TASK|>
Adjust your previous direction according to the feedback:
<|CLOSE_SECTION|>
{% endif -%}
{{ set_prepared_response("Director instructs "+character.name+": \"To progress the scene, i want you to ") }}

View File

@@ -1,22 +0,0 @@
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:DIALOGUE ANALYSIS|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
{% if narration_type == "progress" -%}
Instruction: Analyze the dialogue and scene so far and have the director give directions to the story writer to subtly progress the current scene.
{% elif narration_type == "visual" %}
Instruction: Analyze the last line of the dialogue and have the director give directions to the story writer to describe the end point of the scene visually.
{% elif narration_type == "character" %}
{% endif -%}
{% if scene.history -%}
Last line of dialogue: {{ scene.history[-1] }}
{% endif -%}
{{ current_goal }}
<|CLOSE_SECTION|>
{{ bot_token }}Director instructs story writer:

View File

@@ -0,0 +1,15 @@
<|SECTION:SCENE|>
{% block scene_history -%}
{% for scene_context in scene.context_history(budget=1000, min_dialogue=25, sections=False, keep_director=False) -%}
{{ scene_context }}
{% endfor %}
{% endblock -%}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Current scene goal: {{ prompt }}
Give actionable directions to the actor playing {{ character.name }} by instructing {{ character.name }} to do or say something to progress the scene subtly towards meeting the condition of the current goal.
Take the most recent update to the scene into consideration: {{ scene.history[-1] }}
<|CLOSE_SECTION|>
{{ set_prepared_response("Director instructs "+character.name+": \"To progress the scene, i want you to ") }}

View File

@@ -1,8 +0,0 @@
{% for scene_context in scene.context_history(budget=max_tokens-300) -%}
{{ scene_context }}
{% endfor %}
Question: Do any lines or events in the dialogue satisfy the following story condition: "{{ current_goal }}" - Explain your reasoning and then state 'satisfied' or 'NOT been satisfied'.
{{ bot_token }}Director decides: The condition has

View File

@@ -1,28 +0,0 @@
<|SECTION:CONTEXT|>
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:TASK|>
Instruction: Analyze the scene so far and answer the following question(s)
Expected response: a JSON response containing questions, answers and reasoning
{% if scene.history -%}
Last line of dialogue: {{ scene.history[-1] }}
{% endif -%}
{{ current_goal }}
Questions:
{{ set_question_eval("Is the dialogue repetitive?", "yes", "direct") }}
{{ set_question_eval("Is the actor playing "+character.name+" staying true to the character and their development so far?", "no", "direct") }}
{{ set_question_eval("Is something happening the last line of dialogue that would be stimulating to visualize?", "yes", "narrate:visual") }}
{{ set_question_eval("Is right now a good time to interrupt the dialogue and move the story towards the goal?", "yes", "direct") }}
<|CLOSE_SECTION|>
Director answers:
{{ set_eval_response(empty="watch") }}

View File

@@ -1,20 +0,0 @@
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
{% for scene_context in scene.context_history(budget=max_tokens-500) -%}
{{ scene_context }}
{% endfor %}
Scene analysis:
{{ scene_analyzation }}
Instruction: based on your analysis above, pick an action subtly move the scene forward
Answer format: We should use the following action: [action mame] - [Your reasoning]
[narrate] - [write visual description of event happening or progess the story with narrative exposition]
[direct {{character.name}}] - [direct the actor playing {{character.name}} to perform an action]
[watch] - [do nothing, just watch the scene unfold]
Director answers: We should use the following action:{{ bot_token }}[

View File

@@ -1,16 +0,0 @@
{{ direction_prompt }}
<|SECTION:DIRECTION|>
{{ direction }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Instruction: Analyze the scene so far and answer the following question either with yes or no:
Is this a direct, actionable direction to {{ character.name }} ?
Is the director's instruction to {{ character.name }} in line with the character's development so far?
Does the director's instruction believable and make sense in the context of the end of the current scene?
Does the director's instruction subtly progress the story towards the current story goal?
<|CLOSE_SECTION|>
Director answers:

View File

@@ -1,19 +0,0 @@
{{ direction_prompt }}
<|SECTION:DIRECTION|>
{{ direction }}
<|CLOSE_SECTION|>
<|SECTION:ANALYSIS OF DIRECTION|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Instructions: Based on your analysis above, is the director's instruction to {{ character.name }} good, neutral or bad? If its bad, change the direction. Never question the goal itself. Explain your reasoning.
Expected response: Respond with I want to keep OR change the direction.
Response example: I want to keep the direction, because ..
Response example: I want to change the direction, because ..
<|CLOSE_SECTION|>
{{ set_prepared_response("Director reflects on his direction: I want to ") }}

View File

@@ -1,32 +0,0 @@
<|SECTION:CONTEXT|>
{{ character.description }}
{{ character.base_attributes.get("scenario_context", "") }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:DIALOGUE ANALYSIS|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:STORY GOAL|>
{{ current_goal }}
<|CLOSE_SECTION|>
{% if not previous_direction -%}
<|SECTION:TASK|>
Give actionable directions to the actor playing {{ character.name }} by instructing {{ character.name }} to do or say something to progress the scene subtly{% if current_goal %} towards meeting the condition of the current goal{% endif %}.
<|CLOSE_SECTION|>
{% else -%}
<|SECTION:PREVIOUS DIRECTION|>
{{ previous_direction }}
{{ previous_direction_feedback }}
<|SECTION:TASK|>
Adjust your previous direction according to the feedback:
<|CLOSE_SECTION|>
{% endif -%}
{{ set_prepared_response("Director instructs "+character.name+": \"To progress the scene, i want you to ") }}

View File

@@ -1,22 +0,0 @@
{% for scene_context in scene.context_history(budget=200, add_archieved_history=False, min_dialogue=10) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:DIALOGUE ANALYSIS|>
{{ analysis }}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
{% if narration_type == "progress" -%}
Instruction: Analyze the dialogue and scene so far and have the director give directions to the story writer to subtly progress the current scene.
{% elif narration_type == "visual" %}
Instruction: Analyze the last line of the dialogue and have the director give directions to the story writer to describe the end point of the scene visually.
{% elif narration_type == "character" %}
{% endif -%}
{% if scene.history -%}
Last line of dialogue: {{ scene.history[-1] }}
{% endif -%}
{{ current_goal }}
<|CLOSE_SECTION|>
{{ bot_token }}Director instructs story writer:

View File

@@ -1,8 +0,0 @@
{% for scene_context in scene.context_history(budget=max_tokens-300) -%}
{{ scene_context }}
{% endfor %}
Question: Do any lines or events in the dialogue satisfy the following story condition: "{{ current_goal }}" - Explain your reasoning and then state 'satisfied' or 'NOT been satisfied'.
{{ bot_token }}Director decides: The condition has

View File

@@ -1,15 +1,12 @@
<|SECTION:CONTEXT|>
Scenario Premise: {{ scene.description }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=max_tokens-300) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:TASK|>
Question: What happens at the end of the dialogue progression? Summarize into narrative description.
<|SECTION:CONTEXT|>
Content Context: This is a specific scene from {{ scene.context }}
Narration style: point and click adventure game from the 90s
Expected Answer: A summarized narrative description of the scene unfolding at the dialogue that can be inserted into the ongoing story in place of the dialogue.
Scenario Premise: {{ scene.description }}
<|CLOSE_SECTION|>
Narrator answers: {{ set_prepared_response("You see ") }}
<|SECTION:TASK|>
Provide a visual description of what is currently happening in the scene. Don't progress the scene.
<|CLOSE_SECTION|>
{{ bot_token }}At the end of the scene we currently see:

View File

@@ -0,0 +1,16 @@
<|SECTION:CONTEXT|>
Scenario Premise: {{ scene.description }}
NPCs: {{ scene.npc_character_names }}
Player Character: {{ scene.get_player_character().name }}
Content Context: {{ scene.context }}
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=max_tokens-300) -%}
{{ scene_context }}
{% endfor %}
<|SECTION:TASK|>
Narrate the passage of time that just occured, subtly move the story forward, and set up the next scene.
Write 1 to 3 sentences.
<|CLOSE_SECTION|>
{{ bot_token }}{{ narrative }}:

View File

@@ -6,8 +6,4 @@
Question: What happens within the dialogue? Summarize into narrative description.
Content Context: This is a specific scene from {{ scene.context }}
Expected Answer: A summarized narrative description of the dialogue that can be inserted into the ongoing story in place of the dialogue.
Include implied time skips (for example characters plan to meet at a later date and then they meet).
<|CLOSE_SECTION|>
Narrator answers:

View File

@@ -1,3 +1,4 @@
{{ text }}
<|SECTION:TASK|>

View File

@@ -0,0 +1,16 @@
<|SECTION:CONTEXT|>
{% for memory in query_memory(text, as_question_answer=False, max_tokens=max_tokens-500, iterate=20) -%}
{{ memory }}
{% endfor -%}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Answer the following questions:
{{ query_text("What are 1 to 3 questions to ask the narrator of the story to gather more context from the past for the continuation of this conversation? If a character is asking about a status, location or information about an item or another character, make sure to include question(s) that help gather context for this. Don't explain your reasoning. Don't ask the actors directly.", text, as_question_answer=False) }}
You answers should be precise, truthful and short.
<|CLOSE_SECTION|>
<|SECTION:RELEVANT CONTEXT|>

View File

@@ -0,0 +1,30 @@
<|SECTION:CHARACTERS|>
Player / main character:
- {{ scene.get_player_character().name }}
Other characters:
{% for name in scene.npc_character_names -%}
- {{ name }}
{% endfor -%}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Match the following character aliases to the existing characters.
Respond in the following JSON format:
{
"matched_names": [
{
"alias": "alias", # given alias name for the task
"matched_name": "character name" # name of the character
}
]
}
If the name cannot be matched to a character, skip it
<|CLOSE_SECTION|>
<|SECTION:ALIASES|>
{% for name in names -%}
- {{ name }}
{% endfor -%}
<|CLOSE_SECTION|>
{{ set_json_response(dict(matched_names=[""])) }}

View File

@@ -1,11 +1,57 @@
Instructions: Mark all tangible physical subjects in the sentence with brackets. For example, if the line of dialogue is "John: I am going to the store." and you want to mark "store" as a subject, you would write "John: I am going to [the store]."
<|SECTION:JSON SCHEMA|>
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"characters": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"snapshot": {
# describe the character's current state in the scene
"type": "string"
},
"emotion": {
# simple, one word e.g., "happy", "sad", "angry", "confused", "scared" etc.,
"type": "string"
}
},
"required": ["snapshot", "emotion"]
}
},
"items": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"snapshot": {
# describe the item's current state in the scene
"type": "string"
}
},
"required": ["snapshot"]
}
},
"location": {
# where is the scene taking place?
"type": "string"
}
},
"required": ["characters", "items", "location"]
}
<|CLOSE_SECTION|>
<|SECTION:LAST KNOWN WORLD STATE|>
{{ scene.world_state.pretty_json }}
<|CLOSE_SECTION|>
<|SECTION:SCENE PROGRESS|>
{% for scene_context in scene.context_history(budget=300, min_dialogue=5, add_archieved_history=False, max_dialogue=5) -%}
{{ scene_context }}
{% endfor -%}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Update the existing JSON object for the world state to reflect the changes in the scene progression.
Sentence:
Barbara: *Barabara sits down on the couch while John is watching TV* Lets see whats on *She takes the remote and starts flipping through channels. She occasionally snaps her wristband while she does it*
Sentence with tangible physical objects marked:
Barbara: *Barabara sits down on [the couch] while John is watching [TV]* Lets see whats on *She takes [the remote] and starts flipping through [channels]. She occasionally snaps [her wristband] while she does it*
Sentence:
{{ scene.history[-1] }}
Sentence with tangible physical objects marked::{{ bot_token }}
Objects that are no longer explicitly mentioned in the scene progression should be removed from the JSON object.
<|CLOSE_SECTION|>
<|SECTION:UPDATED WORLD STATE|>{{ set_json_response(dict(characters={"name":{}}), cutoff=1) }}

View File

@@ -0,0 +1,56 @@
<|SECTION:EXAMPLE|>
{
"characters": {
# the character name is the key
"Character name": {
"emotion": "The current emotional state or mood of the character. (neutral, happy, sad, angry, etc.)",
"snapshot": "A brief narrative description of what the character is doing at this moment in the scene."
},
# ...
},
"items": {
# the item name is the key in natural language (short)
"Item name": {
"snapshot": "A brief narrative description of the item and the state its currently in."
},
# ...
},
"location": "A brief narrative description of the location the scene is taking place in.",
}
<|CLOSE_SECTION|>
<|SECTION:CONTEXT|>
Player character: {{ scene.get_player_character().name }}
Other major characters:
{% for npc_name in scene.npc_character_names -%}
{{ npc_name }}
{% endfor -%}
{% for scene_context in scene.context_history(budget=1000, min_dialogue=10, dialogue_negative_offset=5, sections=False) -%}
{{ scene_context }}
{% endfor -%}
{% if not scene.history -%}
<|SECTION:DIALOGUE|>
No dialogue so far
{% endif -%}
<|CLOSE_SECTION|>
<|SECTION:SCENE PROGRESS|>
{% for scene_context in scene.context_history(budget=300, min_dialogue=5, add_archieved_history=False, max_dialogue=5) -%}
{{ scene_context }}
{% endfor -%}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Create a JSON object for the world state that reflects the scene progression so far.
The world state needs to include important concrete and material items present at the very end of the dialogue.
The world state needs to include persons (characters) interacting at the very end of the dialogue
Be factual and truthful. Don't make up things that are not in the context or dialogue.
Snapshot text should always be specified. If you don't know what to write, write "You see nothing special."
Emotion should always be specified. If you don't know what to write, write "neutral".
Required response: a complete and valid JSON response according to the JSON example containing items and characters.
characters should have the following attributes: `emotion`, `snapshot`
items should have the following attributes: `snapshot`
<|CLOSE_SECTION|>
<|SECTION:UPDATED WORLD STATE|>
{{ set_json_response(dict(characters={"name":{}}), cutoff=3) }}

View File

@@ -1,24 +1,22 @@
<|SECTION:CONTEXT EXAMPLE|>
Barbara visited her borther John.
<|CLOSE_SECTION|>
<|SECTION:DIALOGUE EXAMPLE|>
Barbara: *Barbara accidently poured some yoghurt on her shirt*
John: I love filming myself *Holds up his phone to film himself* I dont mind that the screen is cracked!
Barbara: I should change this shirt but i dont want to get up from the couch
<|CLOSE_SECTION|>
<|SECTION:WORLD STATE EXAMPLE|>
<|SECTION:WORLD STATE SCHEMA|>
{
"items": [
{"name": "Barbara's red shirt", "snapshot": "The shirt has a big stain on it"},
{"name": "John's fanncy phone", "snapshot": "The screen is cracked"}
],
"characters": [
{"name": "John", "emotion": "Excited", "snapshot": "John is filming himself on his phone next to his sister"},
{"name": "Barbara", "emotion": "Calm", "snapshot": "Barbara is sitting on the couch"}
{
"name": "The name of the character involved in the scene.",
"emotion": "The current emotional state or mood of the character.",
"snapshot": "A brief description of what the character is doing at this moment in the scene."
},
# ...
],
"items": [
{
"name": "The name of an item that belongs to one of the characters.",
"snapshot": "A brief description of the item's current condition or any notable features."
},
# ...
]
}
<|CLOSE_SECTION|>
<|SECTION:CONTEXT|>
{% for scene_context in scene.context_history(budget=1000, min_dialogue=10, dialogue_negative_offset=5, sections=False) -%}
@@ -46,10 +44,10 @@ Required response: a complete and valid JSON response according to the JSON exam
characters should habe the following attributes: `name`, `emotion`, `snapshot`
items should have the following attributes: `name`, `snapshot`
Don't copy the example, write your own descriptions.
You must not copy the example, write your own descriptions.
<|CLOSE_SECTION|>
{% for scene_context in scene.context_history(budget=300, min_dialogue=5, add_archieved_history=False, max_dialogue=5) -%}
{{ scene_context }}
{% endfor -%}
<|SECTION:WORLD STATE|>
{{ set_json_response(dict(items=[""])) }}
{{ set_json_response(dict(characters=[{"name":scene.character_names[0]}])) }}

View File

@@ -63,8 +63,8 @@ class WebsocketHandler(Receiver):
abort_wait_for_input()
memory_agent = instance.get_agent("memory")
if memory_agent:
memory_agent.close_db()
if memory_agent and self.scene:
memory_agent.close_db(self.scene)
def connect_llm_clients(self):
client = None
@@ -128,6 +128,10 @@ class WebsocketHandler(Receiver):
async def load_scene(self, path_or_data, reset=False, callback=None, file_name=None):
try:
if self.scene:
instance.get_agent("memory").close_db(self.scene)
scene = self.init_scene()
if not scene:
@@ -135,19 +139,10 @@ class WebsocketHandler(Receiver):
return
conversation_helper = scene.get_helper("conversation")
memory_helper = scene.get_helper("memory")
await memory_helper.agent.set_db()
scene = await load_scene(
scene, path_or_data, conversation_helper.agent.client, reset=reset
)
#elif isinstance(path_or_data, dict):
# scene = await load_scene_from_data(
# scene, path_or_data, conversation_helper.agent.client, reset=reset
# )
# Continuously ask the user for input and send it to the actor's talk_to method
self.scene = scene
@@ -281,12 +276,20 @@ class WebsocketHandler(Receiver):
)
def handle_director(self, emission: Emission):
if emission.character:
character = emission.character.name
elif emission.message_object.source:
character = emission.message_object.source
else:
character = ""
self.queue_put(
{
"type": "director",
"message": emission.message,
"id": emission.id,
"character": emission.character.name if emission.character else "",
"character": character,
}
)

View File

@@ -6,6 +6,8 @@ import random
import traceback
import re
import isodate
import uuid
import time
from typing import Dict, List, Optional, Union
from blinker import signal
@@ -521,6 +523,7 @@ class Player(Actor):
return message
async_signals.register("game_loop_start")
async_signals.register("game_loop")
class Scene(Emitter):
@@ -548,6 +551,8 @@ class Scene(Emitter):
self.name = ""
self.filename = ""
self.memory_id = str(uuid.uuid4())[:10]
self.saved = False
self.context = ""
self.commands = commands.Manager(self)
@@ -569,6 +574,7 @@ class Scene(Emitter):
"archive_add": signal("archive_add"),
"character_state": signal("character_state"),
"game_loop": async_signals.get("game_loop"),
"game_loop_start": async_signals.get("game_loop_start"),
}
self.setup_emitter(scene=self)
@@ -584,6 +590,10 @@ class Scene(Emitter):
def character_names(self):
return [character.name for character in self.characters]
@property
def npc_character_names(self):
return [character.name for character in self.get_npc_characters()]
@property
def log(self):
return log
@@ -933,11 +943,12 @@ class Scene(Emitter):
reserved_min_archived_history_tokens = count_tokens(self.archived_history[-1]["text"]) if self.archived_history else 0
reserved_intro_tokens = count_tokens(self.get_intro()) if show_intro else 0
max_dialogue_budget = min(max(budget - reserved_intro_tokens - reserved_min_archived_history_tokens, 1000), budget)
max_dialogue_budget = min(max(budget - reserved_intro_tokens - reserved_min_archived_history_tokens, 500), budget)
dialogue_popped = False
while count_tokens(dialogue) > max_dialogue_budget:
dialogue.pop(0)
dialogue_popped = True
if dialogue:
@@ -949,7 +960,7 @@ class Scene(Emitter):
context_history = [context_history[1]]
# we only have room for dialogue, so we return it
if dialogue_popped:
if dialogue_popped and max_dialogue_budget >= budget:
return context_history
# if we dont have lots of archived history, we can also include the scene
@@ -983,7 +994,6 @@ class Scene(Emitter):
i = len(self.archived_history) - 1
limit = 5
if sections:
context_history.insert(archive_insert_idx, "<|CLOSE_SECTION|>")
@@ -998,6 +1008,7 @@ class Scene(Emitter):
text = self.archived_history[i]["text"]
if count_tokens(context_history) + count_tokens(text) > budget:
break
context_history.insert(archive_insert_idx, text)
i -= 1
limit -= 1
@@ -1055,8 +1066,13 @@ class Scene(Emitter):
new_message = await narrator.agent.narrate_character(character)
elif source == "narrate_query":
new_message = await narrator.agent.narrate_query(arg)
else:
fn = getattr(narrator.agent, source, None)
if not fn:
return
args = arg.split(";") if arg else []
new_message = await fn(*args)
save_source = f"{source}:{arg}" if arg else source
@@ -1085,8 +1101,7 @@ class Scene(Emitter):
director = self.get_helper("director")
response = await director.agent.direct(character)
response = await director.agent.direct_scene(character)
if not response:
log.info("Director returned no response")
return
@@ -1153,9 +1168,12 @@ class Scene(Emitter):
"assets": self.assets.dict(),
"characters": [actor.character.serialize for actor in self.actors],
"scene_time": util.iso8601_duration_to_human(self.ts, suffix="") if self.ts else None,
"saved": self.saved,
},
)
self.log.debug("scene_status", scene=self.name, scene_time=self.ts, saved=self.saved)
def set_environment(self, environment: str):
"""
Set the environment of the scene
@@ -1177,11 +1195,20 @@ class Scene(Emitter):
Loops through self.history looking for TimePassageMessage and will
advance the world state by the amount of time passed for each
"""
# reset time
self.ts = "PT0S"
# archived history (if "ts" is set) should provide the base line
# find the first archived_history entry from the back that has a ts
# and set that as the base line
if self.archived_history:
for i in range(len(self.archived_history) - 1, -1, -1):
if self.archived_history[i].get("ts"):
self.ts = self.archived_history[i]["ts"]
break
for message in self.history:
if isinstance(message, TimePassageMessage):
self.advance_time(message.ts)
@@ -1283,6 +1310,8 @@ class Scene(Emitter):
self.active_actor = None
self.next_actor = None
await self.signals["game_loop_start"].send(events.GameLoopStartEvent(scene=self, event_type="game_loop_start"))
while continue_scene:
try:
@@ -1312,6 +1341,8 @@ class Scene(Emitter):
await self.call_automated_actions()
continue
self.saved = False
# Store the most recent AI Actor
self.most_recent_ai_actor = actor
@@ -1319,6 +1350,9 @@ class Scene(Emitter):
emit(
"character", item, character=actor.character
)
self.emit_status()
except TalemateInterrupt:
raise
except LLMAccuracyError as e:
@@ -1349,6 +1383,10 @@ class Scene(Emitter):
continue
await command.execute(message)
self.saved = False
self.emit_status()
except TalemateInterrupt:
raise
except LLMAccuracyError as e:
@@ -1375,12 +1413,14 @@ class Scene(Emitter):
return saves_dir
async def save(self):
async def save(self, save_as:bool=False):
"""
Saves the scene data, conversation history, archived history, and characters to a json file.
"""
scene = self
if save_as:
self.filename = None
if not self.name:
self.name = await wait_for_input("Enter scenario name: ")
@@ -1390,6 +1430,13 @@ class Scene(Emitter):
self.filename = await wait_for_input("Enter save name: ")
self.filename = self.filename.replace(" ", "-").lower()+".json"
if save_as:
memory_agent = self.get_helper("memory").agent
memory_agent.close_db(self)
self.memory_id = str(uuid.uuid4())[:10]
await memory_agent.set_db()
await self.commit_to_memory()
saves_dir = self.save_dir
log.info(f"Saving to: {saves_dir}")
@@ -1412,6 +1459,7 @@ class Scene(Emitter):
"context": scene.context,
"world_state": scene.world_state.dict(),
"assets": scene.assets.dict(),
"memory_id": scene.memory_id,
"ts": scene.ts,
}
@@ -1420,8 +1468,35 @@ class Scene(Emitter):
with open(filepath, "w") as f:
json.dump(scene_data, f, indent=2, cls=save.SceneEncoder)
self.saved = True
self.emit_status()
async def commit_to_memory(self):
# will recommit scene to long term memory
memory = self.get_helper("memory").agent
memory.drop_db()
await memory.set_db()
for ah in self.archived_history:
ts = ah.get("ts", "PT1S")
if not ah.get("ts"):
ah["ts"] = ts
self.signals["archive_add"].send(
events.ArchiveEvent(scene=self, event_type="archive_add", text=ah["text"], ts=ts)
)
await asyncio.sleep(0)
for character_name, cs in self.character_states.items():
self.set_character_state(character_name, cs)
for character in self.characters:
await character.commit_to_memory(memory)
def reset(self):
self.history = []
self.archived_history = []

View File

@@ -572,7 +572,7 @@ def iso8601_duration_to_human(iso_duration, suffix:str=" ago"):
elif components:
human_str = components[0]
else:
human_str = "0 Seconds"
human_str = "Moments"
return f"{human_str}{suffix}"
@@ -766,99 +766,131 @@ def replace_exposition_markers(s:str) -> str:
def ensure_dialog_format(line:str, talking_character:str=None) -> str:
line = mark_exposition(line, talking_character)
line = mark_spoken_words(line, talking_character)
return line
def mark_spoken_words(line:str, talking_character:str=None) -> str:
# if there are no asterisks in the line, it means its impossible to tell
# dialogue apart from exposition
if "*" not in line:
return line
if talking_character and line.startswith(f"{talking_character}:"):
line = line[len(talking_character)+1:].lstrip()
# Splitting the text into segments based on asterisks
segments = re.split('(\*[^*]*\*)', line)
formatted_line = ""
for i, segment in enumerate(segments):
if segment.startswith("*") and segment.endswith("*"):
# If the segment is an action or thought, add it as is
formatted_line += segment
else:
# For non-action/thought parts, trim and add quotes only if not empty and not already quoted
trimmed_segment = segment.strip()
if trimmed_segment:
if not (trimmed_segment.startswith('"') and trimmed_segment.endswith('"')):
formatted_line += f' "{trimmed_segment}"'
else:
formatted_line += f' {trimmed_segment}'
# adds spaces betwen *" and "* to make it easier to read
formatted_line = formatted_line.replace('*"', '* "')
formatted_line = formatted_line.replace('"*', '" *')
#if "*" not in line and '"' not in line:
# if talking_character:
# line = line[len(talking_character)+1:].lstrip()
# return f"{talking_character}: \"{line}\""
# return f"\"{line}\""
#
if talking_character:
formatted_line = f"{talking_character}: {formatted_line}"
log.debug("mark_spoken_words", line=line, formatted_line=formatted_line)
return formatted_line.strip() # Trim any leading/trailing whitespace
def mark_exposition(line:str, talking_character:str=None) -> str:
"""
Will loop through the string and make sure chunks outside of "" are marked with *.
For example:
"No, you're not wrong" sips his wine "This tastes gross." coughs "acquired taste i guess?"
becomes
"No, you're not wrong" *sips his wine* "This tastes gross." *coughs* "acquired taste i guess?"
"""
# no quotes in string, means its impossible to tell dialogue apart from exposition
if '"' not in line:
return line
if talking_character and line.startswith(f"{talking_character}:"):
line = line[len(talking_character)+1:].lstrip()
# Splitting the text into segments based on quotes
segments = re.split('("[^"]*")', line)
formatted_line = ""
lines = []
for i, segment in enumerate(segments):
# If the segment is a spoken part (inside quotes), add it as is
if segment.startswith('"') and segment.endswith('"'):
formatted_line += segment
else:
# Split the non-spoken segment into sub-segments based on existing asterisks
sub_segments = re.split('(\*[^*]*\*)', segment)
for sub_segment in sub_segments:
if sub_segment.startswith("*") and sub_segment.endswith("*"):
# If the sub-segment is already formatted, add it as is
formatted_line += sub_segment
else:
# Trim and add asterisks only to non-empty sub-segments
trimmed_sub_segment = sub_segment.strip()
if trimmed_sub_segment:
formatted_line += f" *{trimmed_sub_segment}*"
for _line in line.split("\n"):
_line = ensure_dialog_line_format(_line)
# adds spaces betwen *" and "* to make it easier to read
formatted_line = formatted_line.replace('*"', '* "')
formatted_line = formatted_line.replace('"*', '" *')
lines.append(_line)
if len(lines) > 1:
line = "\n".join(lines)
else:
line = lines[0]
if talking_character:
formatted_line = f"{talking_character}: {formatted_line}"
log.debug("mark_exposition", line=line, formatted_line=formatted_line)
line = f"{talking_character}: {line}"
return line
return formatted_line.strip() # Trim any leading/trailing whitespace
def ensure_dialog_line_format(line:str):
"""
a Python function that standardizes the formatting of dialogue and action/thought
descriptions in text strings. This function is intended for use in a text-based
game where spoken dialogue is encased in double quotes (" ") and actions/thoughts are
encased in asterisks (* *). The function must correctly format strings, ensuring that
each spoken sentence and action/thought is properly encased
"""
i = 0
segments = []
segment = None
segment_open = None
for i in range(len(line)):
c = line[i]
#print("segment_open", segment_open)
#print("segment", segment)
if c in ['"', '*']:
if segment_open == c:
# open segment is the same as the current character
# closing
segment_open = None
segment += c
segments += [segment.strip()]
segment = None
elif segment_open is not None and segment_open != c:
# open segment is not the same as the current character
# opening - close the current segment and open a new one
segments += [segment.strip()]
segment_open = c
segment = c
elif segment_open is None:
# we're opening a segment
segment_open = c
segment = c
else:
if segment_open is None:
segment_open = "unclassified"
segment = c
else:
segment += c
if segment is not None:
segments += [segment.strip()]
for i in range(len(segments)):
segment = segments[i]
if segment in ['"', '*']:
if i > 0:
prev_segment = segments[i-1]
if prev_segment[-1] not in ['"', '*']:
segments[i-1] = f"{prev_segment}{segment}"
segments[i] = ""
continue
for i in range(len(segments)):
segment = segments[i]
if not segment:
continue
if segment[0] == "*" and segment[-1] != "*":
segment += "*"
elif segment[-1] == "*" and segment[0] != "*":
segment = "*" + segment
elif segment[0] == '"' and segment[-1] != '"':
segment += '"'
elif segment[-1] == '"' and segment[0] != '"':
segment = '"' + segment
elif segment[0] in ['"', '*'] and segment[-1] == segment[0]:
continue
segments[i] = segment
for i in range(len(segments)):
segment = segments[i]
if not segment or segment[0] in ['"', '*']:
continue
prev_segment = segments[i-1] if i > 0 else None
next_segment = segments[i+1] if i < len(segments)-1 else None
if prev_segment and prev_segment[-1] == '"':
segments[i] = f"*{segment}*"
elif prev_segment and prev_segment[-1] == '*':
segments[i] = f"\"{segment}\""
elif next_segment and next_segment[0] == '"':
segments[i] = f"*{segment}*"
elif next_segment and next_segment[0] == '*':
segments[i] = f"\"{segment}\""
return " ".join(segment for segment in segments if segment)

View File

@@ -39,13 +39,16 @@ class WorldState(BaseModel):
def as_list(self):
return self.render().as_list
def reset(self):
self.characters = {}
self.items = {}
self.location = None
def emit(self, status="update"):
emit("world_state", status=status, data=self.dict())
async def request_update(self, initial_only:bool=False):
if initial_only and self.characters:
self.emit()
return
@@ -58,19 +61,94 @@ class WorldState(BaseModel):
self.emit()
raise e
previous_characters = self.characters
previous_items = self.items
scene = self.agent.scene
character_names = scene.character_names
self.characters = {}
self.items = {}
for character in world_state.get("characters", []):
self.characters[character["name"]] = CharacterState(**character)
for character_name, character in world_state.get("characters", {}).items():
# character name may not always come back exactly as we have
# it defined in the scene. We assign the correct name by checking occurences
# of both names in each other.
if character_name not in character_names:
for _character_name in character_names:
if _character_name.lower() in character_name.lower() or character_name.lower() in _character_name.lower():
log.debug("world_state adjusting character name", from_name=character_name, to_name=_character_name)
character_name = _character_name
break
if not character:
continue
# if emotion is not set, see if a previous state exists
# and use that emotion
if "emotion" not in character:
log.debug("emotion not set", character_name=character_name, character=character, characters=previous_characters)
if character_name in previous_characters:
character["emotion"] = previous_characters[character_name].emotion
self.characters[character_name] = CharacterState(**character)
log.debug("world_state", character=character)
for item in world_state.get("items", []):
self.items[item["name"]] = ObjectState(**item)
for item_name, item in world_state.get("items", {}).items():
if not item:
continue
self.items[item_name] = ObjectState(**item)
log.debug("world_state", item=item)
await self.persist()
self.emit()
async def persist(self):
memory = instance.get_agent("memory")
world_state = instance.get_agent("world_state")
# first we check if any of the characters were refered
# to with an alias
states = []
scene = self.agent.scene
for character_name in self.characters.keys():
states.append(
{
"text": f"{character_name}: {self.characters[character_name].snapshot}",
"id": f"{character_name}.world_state.snapshot",
"meta": {
"typ": "world_state",
"character": character_name,
"ts": scene.ts,
}
}
)
for item_name in self.items.keys():
states.append(
{
"text": f"{item_name}: {self.items[item_name].snapshot}",
"id": f"{item_name}.world_state.snapshot",
"meta": {
"typ": "world_state",
"item": item_name,
"ts": scene.ts,
}
}
)
log.debug("world_state.persist", states=states)
if not states:
return
await memory.add_many(states)
async def request_update_inline(self):

View File

@@ -91,7 +91,6 @@ export default {
openModal() {
this.state.formTitle = 'Add AI Agent';
this.state.dialog = true;
console.log("got here")
},
saveAgent(agent) {
const index = this.state.agents.findIndex(c => c.name === agent.name);
@@ -120,7 +119,6 @@ export default {
handleMessage(data) {
// Handle agent_status message type
if (data.type === 'agent_status') {
console.log("agents: got agent_status message", data)
// Find the client with the given name
const agent = this.state.agents.find(agent => agent.name === data.name);
if (agent) {

View File

@@ -26,7 +26,7 @@
hide-details
v-model="client.max_token_length"
:min="1024"
:max="16384"
:max="128000"
:step="512"
@update:modelValue="saveClient(client)"
@click.stop

View File

@@ -17,7 +17,7 @@
</v-card-title>
<v-card-text>
<v-card-text class="scrollable-content">
<v-select v-model="agent.client" :items="agent.data.client" label="Client"></v-select>
<v-alert type="warning" variant="tonal" density="compact" v-if="agent.data.experimental">
@@ -32,11 +32,13 @@
<v-card-text>
{{ agent.data.actions[key].description }}
<div v-for="(action_config, config_key) in agent.data.actions[key].config" :key="config_key">
<div v-if="action.enabled">
<!-- render config widgets based on action_config.type (int, str, bool, float) -->
<v-text-field v-if="action_config.type === 'str'" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact"></v-text-field>
<v-text-field v-if="action_config.type === 'text'" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact"></v-text-field>
<v-slider v-if="action_config.type === 'number' && action_config.step !== null" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" :min="action_config.min" :max="action_config.max" :step="action_config.step" density="compact" thumb-label></v-slider>
<v-checkbox v-if="action_config.type === 'bool'" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact"></v-checkbox>
</div>
</div>
</v-card-text>
</v-card>
@@ -98,3 +100,11 @@ export default {
}
}
</script>
<style>
.scrollable-content {
overflow-y: auto;
max-height: 70vh;
padding-right: 16px;
}
</style>

View File

@@ -18,7 +18,7 @@
<v-row>
<v-col cols="12">
<v-text-field v-model="client.apiUrl" v-if="client.type === 'textgenwebui'" label="API URL"></v-text-field>
<v-select v-model="client.model" v-if="client.type === 'openai'" :items="['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k']" label="Model"></v-select>
<v-select v-model="client.model" v-if="client.type === 'openai'" :items="['gpt-4-1106-preview', 'gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k']" label="Model"></v-select>
</v-col>
</v-row>
<v-row>

View File

@@ -1,6 +1,7 @@
<template>
<v-list-subheader class="text-uppercase"><v-icon>mdi-post-outline</v-icon> Prompts
<v-chip size="x-small" color="primary">{{ max_prompts }}</v-chip>
<v-icon color="primary" class="ml-2" @click="clearPrompts">mdi-close</v-icon>
</v-list-subheader>
<v-list-item density="compact">
@@ -9,15 +10,19 @@
<v-list-item v-for="(prompt, index) in prompts" :key="index" @click="openPromptView(prompt)">
<v-list-item-title class="text-caption">
{{ prompt.kind }}
<v-row>
<v-col cols="2" class="text-info">#{{ prompt.num }}</v-col>
<v-col cols="10" class="text-right">{{ prompt.kind }}</v-col>
</v-row>
</v-list-item-title>
<v-list-item-subtitle>
<v-chip size="x-small"><v-icon size="14"
class="mr-1">mdi-pound</v-icon>{{ prompt.num }}</v-chip>
<v-chip size="x-small" color="primary">{{ prompt.prompt_tokens }}<v-icon size="14"
<v-chip size="x-small" class="mr-1" color="primary">{{ prompt.prompt_tokens }}<v-icon size="14"
class="ml-1">mdi-arrow-down-bold</v-icon></v-chip>
<v-chip size="x-small" color="secondary">{{ prompt.response_tokens }}<v-icon size="14"
<v-chip size="x-small" class="mr-1" color="secondary">{{ prompt.response_tokens }}<v-icon size="14"
class="ml-1">mdi-arrow-up-bold</v-icon></v-chip>
<v-chip size="x-small">{{ prompt.time }}s<v-icon size="14" class="ml-1">mdi-clock</v-icon></v-chip>
</v-list-item-subtitle>
<v-divider class="mt-1"></v-divider>
</v-list-item>
@@ -33,7 +38,7 @@ export default {
data() {
return {
prompts: [],
total: 0,
total: 1,
max_prompts: 50,
}
},
@@ -47,6 +52,10 @@ export default {
],
methods: {
clearPrompts() {
this.prompts = [];
this.total = 0;
},
handleMessage(data) {
if(data.type === "system"&& data.id === "scene.loaded") {
@@ -63,6 +72,7 @@ export default {
kind: data.data.kind,
response_tokens: data.data.response_tokens,
prompt_tokens: data.data.prompt_tokens,
time: parseInt(data.data.time),
num: this.total++,
})

View File

@@ -1,13 +1,15 @@
<template>
<div class="director-container" v-if="show && minimized" >
<v-chip closable @click:close="deleteMessage()" color="deep-purple-lighten-3">
<v-chip closable color="deep-orange" class="clickable" @click:close="deleteMessage()">
<v-icon class="mr-2">mdi-bullhorn-outline</v-icon>
<span @click="toggle()">{{ character }}</span>
</v-chip>
</div>
<v-alert v-else-if="show" class="director-message" variant="text" :closable="message_id !== null" type="info" icon="mdi-bullhorn-outline"
<v-alert v-else-if="show" color="deep-orange" class="director-message clickable" variant="text" type="info" icon="mdi-bullhorn-outline"
elevation="0" density="compact" @click:close="deleteMessage()" >
<div class="director-text" @click="toggle()">{{ text }}</div>
<span class="director-instructs" @click="toggle()">{{ directorInstructs }}</span>
<span class="director-character ml-1 text-decoration-underline" @click="toggle()">{{ directorCharacter }}</span>
<span class="director-text ml-1" @click="toggle()">{{ directorText }}</span>
</v-alert>
</template>
@@ -21,6 +23,17 @@ export default {
},
props: ['text', 'message_id', 'character'],
inject: ['requestDeleteMessage'],
computed: {
directorInstructs() {
return "Director instructs"
},
directorCharacter() {
return this.text.split(':')[0].split("Director instructs ")[1];
},
directorText() {
return this.text.split(':')[1].split('"')[1];
}
},
methods: {
toggle() {
this.minimized = !this.minimized;
@@ -41,6 +54,10 @@ export default {
margin-right: 2px;
}
.clickable {
cursor: pointer;
}
.highlight:before {
--content: "*";
}
@@ -50,16 +67,33 @@ export default {
}
.director-text {
color: #9FA8DA;
}
.director-message {
display: flex;
flex-direction: row;
color: #9FA8DA;
}
.director-container {
}
.director-instructs {
/* Add your CSS styles for "Director instructs" here */
color: #BF360C;
}
.director-character {
/* Add your CSS styles for the character name here */
}
.director-text {
/* Add your CSS styles for the actual instruction here */
color: #EF6C00;
}
.director-text::after {
content: '"';
}
.director-text::before {
content: '"';
}
</style>

View File

@@ -58,6 +58,7 @@ export default {
scenes: [],
sceneSearchInput: null,
sceneSearchLoading: false,
sceneSaved: null,
expanded: true,
}
},
@@ -83,6 +84,13 @@ export default {
this.getWebsocket().send(JSON.stringify({ type: 'load_scene', file_path: "environment:creative" }));
},
loadScene() {
if(this.sceneSaved === false) {
if(!confirm("The current scene is not saved. Are you sure you want to load a new scene?")) {
return;
}
}
this.loading = true;
if (this.inputMethod === 'file' && this.sceneFile.length > 0) { // Check if the input method is "file" and there is at least one file
// Convert the uploaded file to base64
@@ -119,6 +127,12 @@ export default {
return;
}
// Handle scene status
if (data.type == "scene_status") {
this.sceneSaved = data.data.saved;
return;
}
}
},
created() {

View File

@@ -89,7 +89,8 @@
<v-app-bar-nav-icon @click="toggleNavigation('game')"><v-icon>mdi-script</v-icon></v-app-bar-nav-icon>
<v-toolbar-title v-if="scene.name !== undefined">
{{ scene.name || 'Untitled Scenario' }}
<v-chip size="x-small" v-if="scene.environment === 'creative'" class="ml-1"><v-icon text="Creative" size="14"
<span v-if="scene.saved === false" class="text-red">*</span>
<v-chip size="x-small" v-if="scene.environment === 'creative'" class="ml-2"><v-icon text="Creative" size="14"
class="mr-1">mdi-palette-outline</v-icon>Creative Mode</v-chip>
<v-chip size="x-small" v-else-if="scene.environment === 'scene'" class="ml-1"><v-icon text="Play" size="14"
class="mr-1">mdi-gamepad-square</v-icon>Game Mode</v-chip>
@@ -244,8 +245,10 @@ export default {
}
this.connecting = true;
let currentUrl = new URL(window.location.href);
console.log(currentUrl);
this.websocket = new WebSocket('ws://localhost:5050/ws');
this.websocket = new WebSocket(`ws://${currentUrl.hostname}:5050/ws`);
console.log("Websocket connecting ...")
this.websocket.onmessage = this.handleMessage;
this.websocket.onopen = () => {
@@ -300,6 +303,7 @@ export default {
name: data.name,
environment: data.data.environment,
scene_time: data.data.scene_time,
saved: data.data.saved,
}
this.sceneActive = true;
return;

View File

@@ -12,7 +12,7 @@
<v-expansion-panel rounded="0" density="compact">
<v-expansion-panel-title class="text-subtitle-2" diable-icon-rotate>
{{ name }}
<v-chip label size="x-small" variant="outlined" class="ml-1">{{ character.emotion }}</v-chip>
<v-chip v-if="character.emotion !== null && character.emotion !== ''" label size="x-small" variant="outlined" class="ml-1">{{ character.emotion }}</v-chip>
<template v-slot:actions>
<v-icon icon="mdi-account"></v-icon>
</template>

View File

@@ -0,0 +1,4 @@
{{ system_message }}
### Instruction:
{{ set_response(prompt, "\n\n### Response:\n") }}

View File

@@ -0,0 +1 @@
User: {{ system_message }} {{ set_response(prompt, "\nAssistant: ") }}