Compare commits

..

1 Commits

Author SHA1 Message Date
FInalWombat
73240b5791 Prep 0.10.0 (#12)
* track time passage in scene using iso 8601 format

* chromadb openai instructions

model recommendations updated

* time context passed to long term memory

* add some pre-established history for testing purposes

* time passage

analyze dialogue to template

query_text template function

analyze text and answer question summarizer function

llm prompt template adjustments

iso8601 time utils

chromadb docs adjustments

* didnt mean to remove this

* fix ClientContext stacking

* conversation cleanup tweaks

* prompt prepared response padding

* fix some bugs causing conversation lines containing : to be terminated
early

* fixes issue with chara importing dialoge examples as huge blob instea of
splitting into lines

dialogue example in conversation template randomized

* llm prompt template for Speechless-Llama2-Hermes-Orca-Platypus-WizardLM

* version to 0.10.0
2023-10-02 01:38:02 +03:00
37 changed files with 1335 additions and 529 deletions

View File

@@ -102,14 +102,14 @@ Will be updated as i test more models and over time.
| Model Name | Type | Notes |
|-------------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------|
| [GPT-4](https://platform.openai.com/) | Remote | Still the best for consistency and reasoning, but is heavily censored, while talemate will send a general "decensor" system prompt, depending on the type of content you want to roleplay, there is a chance your key will be banned. **If you do use this make sure to monitor your api usage, talemate tends to send a lot more requests than other roleplaying applications.** |
| [Nous Hermes LLama2](https://huggingface.co/TheBloke/Nous-Hermes-Llama2-GPTQ) | 13B model | My go-to model for 13B parameters. It's good at roleplay and also smart enough to handle the world state and narrative tools. A 13B model loaded via exllama also allows you run chromadb with the xl instructor embeddings off of a single 4090. |
| [Xwin-LM-13B](https://huggingface.co/TheBloke/Xwin-LM-13B-V0.1-GPTQ) | 13B model | Really strong model, roleplaying capability still needs more testing |
| [MythoMax](https://huggingface.co/TheBloke/MythoMax-L2-13B-GPTQ) | 13B model | Similar quality to Hermes LLama2, but a bit more creative. Rarely fails on JSON responses. |
| [Synthia v1.2 34B](https://huggingface.co/TheBloke/Synthia-34B-v1.2-GPTQ) | 34B model | Cannot be run at full context together with chromadb instructor models on a single 4090. But a great choice if you're running chromadb with the default embeddings (or on cpu). |
| [Xwin-LM-70B](https://huggingface.co/Xwin-LM/Xwin-LM-70B-V0.1) | 70B model | Great choice if you have the hardware to run it (or can rent it). |
| [Xwin-LM-70B](https://huggingface.co/TheBloke/Xwin-LM-70B-V0.1-GPTQ) | 70B model | Great choice if you have the hardware to run it (or can rent it). |
| [Synthia v1.2 70B](https://huggingface.co/TheBloke/Synthia-70B-v1.2-GPTQ) | 70B model | Great choice if you have the hardware to run it (or can rent it). |
I have not included OpenAI's gpt-3.5-turbo in this list, since it is really inconsistent with JSON responses, plus its probably still just as heavily censored as GPT-4.
| [GPT-4](https://platform.openai.com/) | Remote | Still the best for consistency and reasoning, but is heavily censored. While talemate will send a general "decensor" system prompt, depending on the type of content you want to roleplay, there is a chance your key will be banned. **If you do use this make sure to monitor your api usage, talemate tends to send a lot more requests than other roleplaying applications.** |
| [GPT-3.5-turbo](https://platform.openai.com/) | Remote | It's really inconsistent with JSON responses, plus its probably still just as heavily censored as GPT-4. If you want to run it i'd suggest running it for the conversation agent, and use GPT-4 for the other agents. **If you do use this make sure to monitor your api usage, talemate tends to send a lot more requests than other roleplaying applications.** |
I have not tested with Llama 1 models in a while, Lazarus was really good at roleplay, but started failing on JSON requirements.

View File

@@ -1,7 +1,15 @@
## ChromaDB
# ChromaDB
Talemate uses ChromaDB to maintain long-term memory. The default embeddings used are really fast but also not incredibly accurate. If you want to use more accurate embeddings you can use the instructor embeddings or the openai embeddings. See below for instructions on how to enable these.
In my testing so far, instructor-xl has proved to be the most accurate (even more-so than openai)
## Local instructor embeddings
If you want chromaDB to use the more accurate (but much slower) instructor embeddings add the following to `config.yaml`:
**Note**: The `xl` model takes a while to load even with cuda. Expect a minute of loading time on the first scene you load.
```yaml
chromadb:
embeddings: instructor
@@ -9,17 +17,23 @@ chromadb:
instructor_model: hkunlp/instructor-xl
```
### Instructor embedding models
- `hkunlp/instructor-base` (smallest / fastest)
- `hkunlp/instructor-large`
- `hkunlp/instructor-xl` (largest / slowest) - requires about 5GB of memory
You will need to restart the backend for this change to take effect.
**NOTE** - The first time you do this it will need to download the instructor model you selected. This may take a while, and the talemate backend will be un-responsive during that time.
Once the download is finished, if talemate is still un-responsive, try reloading the front-end to reconnect. When all fails just restart the backend as well.
Once the download is finished, if talemate is still un-responsive, try reloading the front-end to reconnect. When all fails just restart the backend as well. I'll try to make this more robust in the future.
### GPU support
If you want to use the instructor embeddings with GPU support, you will need to install pytorch with CUDA support.
To do this on windows, run `install-pytorch-cuda.bat` from the project root. Then change your device in the config to `cuda`:
To do this on windows, run `install-pytorch-cuda.bat` from the project directory. Then change your device in the config to `cuda`:
```yaml
chromadb:
@@ -28,8 +42,20 @@ chromadb:
instructor_model: hkunlp/instructor-xl
```
Instructor embedding models:
## OpenAI embeddings
- `hkunlp/instructor-base` (smallest / fastest)
- `hkunlp/instructor-large`
- `hkunlp/instructor-xl` (largest / slowest) - requires about 5GB of GPU memory
First make sure your openai key is specified in the `config.yaml` file
```yaml
openai:
api_key: <your-key-here>
```
Then add the following to `config.yaml` for chromadb:
```yaml
chromadb:
embeddings: openai
```
**Note**: As with everything openai, using this isn't free. It's way cheaper than their text completion though. ALSO - if you send super explicit content they may flag / ban your key, so keep that in mind (i hear they usually send warnings first though), and always monitor your usage on their dashboard.

909
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.9.0"
version = "0.10.0"
description = "AI-backed roleplay and narrative tools"
authors = ["FinalWombat"]
license = "GNU Affero General Public License v3.0"
@@ -35,6 +35,7 @@ websockets = "^11.0.3"
structlog = "^23.1.0"
runpod = "==1.2.0"
nest_asyncio = "^1.5.7"
isodate = ">=0.6.1"
# ChromaDB
chromadb = ">=0.4,<1"

View File

@@ -4,7 +4,29 @@
"name": "Infinity Quest",
"history": [],
"environment": "scene",
"archived_history": [],
"ts": "P1Y",
"archived_history": [
{
"text": "Captain Elmer and Kaira first met during their rigorous training for the Infinity Quest mission. Their initial interactions were marked by a sense of mutual respect and curiosity.",
"ts": "PT1S"
},
{
"text": "Over the course of several months, as they trained together, Elmer and Kaira developed a strong bond. They often spent their free time discussing their dreams of exploring the cosmos.",
"ts": "P3M"
},
{
"text": "During a simulated mission, the Starlight Nomad encountered a sudden system malfunction. Elmer and Kaira worked tirelessly together to resolve the issue and avert a potential disaster. This incident strengthened their trust in each other's abilities.",
"ts": "P6M"
},
{
"text": "As they ventured further into uncharted space, the crew faced a perilous encounter with a hostile alien species. Elmer and Kaira's coordinated efforts were instrumental in negotiating a peaceful resolution and avoiding conflict.",
"ts": "P8M"
},
{
"text": "One memorable evening, while gazing at the stars through the ship's observation deck, Elmer and Kaira shared personal stories from their past. This intimate conversation deepened their connection and understanding of each other.",
"ts": "P11M"
}
],
"character_states": {},
"characters": [
{

View File

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

View File

@@ -158,23 +158,16 @@ class ConversationAgent(Agent):
def clean_result(self, result, character):
log.debug("clean result", result=result)
if "#" in result:
result = result.split("#")[0]
result = result.replace("\n", "__LINEBREAK__").strip()
# Removes partial sentence at the end
result = re.sub(r"[^\.\?\!\*]+(\n|$)", "", result)
result = result.replace(" :", ":")
result = result.strip().strip('"').strip()
result = result.replace("[", "*").replace("]", "*")
result = result.replace("(", "*").replace(")", "*")
result = result.replace("**", "*")
result = result.replace("__LINEBREAK__", "\n")
# if there is an uneven number of '*' add one to the end
@@ -237,7 +230,10 @@ class ConversationAgent(Agent):
total_result = total_result.split("#")[0]
# Removes partial sentence at the end
total_result = re.sub(r"[^\.\?\!\*]+(\n|$)", "", total_result)
total_result = util.strip_partial_sentences(total_result)
# Remove "{character.name}:" - all occurences
total_result = total_result.replace(f"{character.name}:", "")
if total_result.count("*") % 2 == 1:
total_result += "*"

View File

@@ -50,14 +50,13 @@ class MemoryAgent(Agent):
def close_db(self):
raise NotImplementedError()
async def add(self, text, character=None, uid=None):
async def add(self, text, character=None, uid=None, ts:str=None, **kwargs):
if not text:
return
log.debug("memory add", text=text, character=character, uid=uid)
await self._add(text, character=character, uid=uid)
await self._add(text, character=character, uid=uid, ts=ts, **kwargs)
async def _add(self, text, character=None):
async def _add(self, text, character=None, ts:str=None, **kwargs):
raise NotImplementedError()
async def add_many(self, objects: list[dict]):
@@ -79,7 +78,7 @@ class MemoryAgent(Agent):
return self.db.get(id)
def on_archive_add(self, event: events.ArchiveEvent):
asyncio.ensure_future(self.add(event.text, uid=event.memory_id))
asyncio.ensure_future(self.add(event.text, uid=event.memory_id, ts=event.ts, typ="history"))
def on_character_state(self, event: events.CharacterStateEvent):
asyncio.ensure_future(
@@ -256,7 +255,7 @@ class ChromaDBMemoryAgent(MemoryAgent):
self.db_client = chromadb.Client(Settings(anonymized_telemetry=False))
openai_key = self.config.get("openai").get("api_key") or os.environ.get("OPENAI_API_KEY"),
openai_key = self.config.get("openai").get("api_key") or os.environ.get("OPENAI_API_KEY")
if openai_key and self.USE_OPENAI:
log.info(
@@ -300,25 +299,35 @@ class ChromaDBMemoryAgent(MemoryAgent):
except ValueError:
pass
async def _add(self, text, character=None, uid=None):
async def _add(self, text, character=None, uid=None, ts:str=None, **kwargs):
metadatas = []
ids = []
await self.emit_status(processing=True)
if character:
metadatas.append({"character": character.name, "source": "talemate"})
meta = {"character": character.name, "source": "talemate"}
if ts:
meta["ts"] = ts
meta.update(kwargs)
metadatas.append(meta)
self.memory_tracker.setdefault(character.name, 0)
self.memory_tracker[character.name] += 1
id = uid or f"{character.name}-{self.memory_tracker[character.name]}"
ids = [id]
else:
metadatas.append({"character": "__narrator__", "source": "talemate"})
meta = {"character": "__narrator__", "source": "talemate"}
if ts:
meta["ts"] = ts
meta.update(kwargs)
metadatas.append(meta)
self.memory_tracker.setdefault("__narrator__", 0)
self.memory_tracker["__narrator__"] += 1
id = uid or f"__narrator__-{self.memory_tracker['__narrator__']}"
ids = [id]
log.debug("chromadb agent add", text=text, meta=meta, id=id)
self.db.upsert(documents=[text], metadatas=metadatas, ids=ids)
await self.emit_status(processing=False)
@@ -341,7 +350,6 @@ class ChromaDBMemoryAgent(MemoryAgent):
metadatas.append(meta)
uid = obj.get("id", f"{character}-{self.memory_tracker[character]}")
ids.append(uid)
self.db.upsert(documents=documents, metadatas=metadatas, ids=ids)
await self.emit_status(processing=False)
@@ -371,14 +379,27 @@ class ChromaDBMemoryAgent(MemoryAgent):
#log.debug("crhomadb agent get", text=text, where=where)
_results = self.db.query(query_texts=[text], where=where)
results = []
for i in range(len(_results["distances"][0])):
await asyncio.sleep(0.001)
distance = _results["distances"][0][i]
doc = _results["documents"][0][i]
meta = _results["metadatas"][0][i]
ts = meta.get("ts")
if distance < 1:
results.append(_results["documents"][0][i])
try:
date_prefix = util.iso8601_diff_to_human(ts, self.scene.ts)
except Exception:
log.error("chromadb agent", error="failed to get date prefix", ts=ts, scene_ts=self.scene.ts)
date_prefix = None
if date_prefix:
doc = f"{date_prefix}: {doc}"
results.append(doc)
else:
break

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
import asyncio
import traceback
from typing import TYPE_CHECKING, Callable, List, Optional, Union
import talemate.data_objects as data_objects
import talemate.util as util
from talemate.prompts import Prompt
from talemate.scene_message import DirectorMessage
from talemate.scene_message import DirectorMessage, TimePassageMessage
from .base import Agent, set_processing
from .registry import register
@@ -14,6 +15,7 @@ from .registry import register
import structlog
import time
import re
log = structlog.get_logger("talemate.agents.summarize")
@@ -40,6 +42,16 @@ class SummarizeAgent(Agent):
super().connect(scene)
scene.signals["history_add"].connect(self.on_history_add)
def clean_result(self, result):
if "#" in result:
result = result.split("#")[0]
# Removes partial sentence at the end
result = re.sub(r"[^\.\?\!]+(\n|$)", "", result)
result = result.strip()
return result
@set_processing
async def build_archive(self, scene):
end = None
@@ -49,16 +61,36 @@ class SummarizeAgent(Agent):
recent_entry = None
else:
recent_entry = scene.archived_history[-1]
start = recent_entry["end"] + 1
start = recent_entry.get("end", 0) + 1
token_threshold = 1300
token_threshold = 1500
tokens = 0
dialogue_entries = []
ts = "PT0S"
time_passage_termination = False
if recent_entry:
ts = recent_entry.get("ts", ts)
for i in range(start, len(scene.history)):
dialogue = scene.history[i]
if isinstance(dialogue, DirectorMessage):
if i == start:
start += 1
continue
if isinstance(dialogue, TimePassageMessage):
log.debug("build_archive", time_passage_message=dialogue)
if i == start:
ts = util.iso8601_add(ts, dialogue.ts)
log.debug("build_archive", time_passage_message=dialogue, start=start, i=i, ts=ts)
start += 1
continue
log.debug("build_archive", time_passage_message_termination=dialogue)
time_passage_termination = True
end = i - 1
break
tokens += util.count_tokens(dialogue)
dialogue_entries.append(dialogue)
if tokens > token_threshold: #
@@ -68,49 +100,65 @@ class SummarizeAgent(Agent):
if end is None:
# nothing to archive yet
return
log.debug("build_archive", start=start, end=end, ts=ts, time_passage_termination=time_passage_termination)
extra_context = None
if recent_entry:
extra_context = recent_entry["text"]
# in order to summarize coherently, we need to determine if there is a favorable
# cutoff point (e.g., the scene naturally ends or shifts meaninfully in the middle
# of the dialogue)
#
# One way to do this is to check if the last line is a TimePassageMessage, which
# indicates a scene change or a significant pause.
#
# If not, we can ask the AI to find a good point of
# termination.
if not time_passage_termination:
# No TimePassageMessage, so we need to ask the AI to find a good point of termination
terminating_line = await self.analyze_dialoge(dialogue_entries)
terminating_line = await self.analyze_dialoge(dialogue_entries)
if terminating_line:
adjusted_dialogue = []
for line in dialogue_entries:
if str(line) in terminating_line:
break
adjusted_dialogue.append(line)
dialogue_entries = adjusted_dialogue
end = start + len(dialogue_entries)
if dialogue_entries:
summarized = await self.summarize(
"\n".join(map(str, dialogue_entries)), extra_context=extra_context
)
else:
# AI has likely identified the first line as a scene change, so we can't summarize
# just use the first line
summarized = str(scene.history[start])
log.debug("summarize agent build archive", terminating_line=terminating_line)
# determine the appropariate timestamp for the summarization
if terminating_line:
adjusted_dialogue = []
for line in dialogue_entries:
if str(line) in terminating_line:
break
adjusted_dialogue.append(line)
dialogue_entries = adjusted_dialogue
end = start + len(dialogue_entries)
summarized = await self.summarize(
"\n".join(map(str, dialogue_entries)), extra_context=extra_context
)
scene.push_archive(data_objects.ArchiveEntry(summarized, start, end))
scene.push_archive(data_objects.ArchiveEntry(summarized, start, end, ts=ts))
return True
@set_processing
async def analyze_dialoge(self, dialogue):
instruction = "Examine the dialogue from the beginning and find the first line that marks a scene change. Repeat the line back to me exactly as it is written"
prepare_response = "The first line that marks a scene change is: "
prompt = dialogue + ["", instruction, f"<|BOT|>{prepare_response}"]
response = await self.client.send_prompt("\n".join(map(str, prompt)), kind="summarize")
if prepare_response in response:
response = response.replace(prepare_response, "")
response = await Prompt.request("summarizer.analyze-dialogue", self.client, "analyze_freeform", vars={
"dialogue": "\n".join(map(str, dialogue)),
"scene": self.scene,
"max_tokens": self.client.max_token_length,
})
response = self.clean_result(response)
return response
@set_processing
async def summarize(
self,
@@ -129,7 +177,7 @@ class SummarizeAgent(Agent):
"max_tokens": self.client.max_token_length,
})
self.scene.log.info("summarize", dialogue=text, response=response)
self.scene.log.info("summarize", dialogue_length=len(text), summarized_length=len(response))
return self.clean_result(response)
@@ -195,4 +243,52 @@ class SummarizeAgent(Agent):
self.scene.log.debug("request_world_state_inline", marked_items=marked_items_response, time=time.time() - t1)
return marked_items_response
return marked_items_response
@set_processing
async def analyze_time_passage(
self,
text: str,
):
response = await Prompt.request(
"summarizer.analyze-time-passage",
self.client,
"analyze_freeform_short",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"text": text,
}
)
duration = response.split("\n")[0].split(" ")[0].strip()
if not duration.startswith("P"):
duration = "P"+duration
return duration
@set_processing
async def analyze_text_and_answer_question(
self,
text: str,
query: str,
):
response = await Prompt.request(
"summarizer.analyze-text-and-answer-question",
self.client,
"analyze_freeform",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"text": text,
"query": query,
}
)
log.debug("analyze_text_and_answer_question", query=query, text=text, response=response)
return response

View File

@@ -4,6 +4,9 @@ Context managers for various client-side operations.
from contextvars import ContextVar
from pydantic import BaseModel, Field
from copy import deepcopy
import structlog
__all__ = [
'context_data',
@@ -11,6 +14,14 @@ __all__ = [
'ContextModel',
]
log = structlog.get_logger()
def model_to_dict_without_defaults(model_instance):
model_dict = model_instance.dict()
for field_name, field in model_instance.__class__.__fields__.items():
if field.default == model_dict.get(field_name):
del model_dict[field_name]
return model_dict
class ConversationContext(BaseModel):
talking_character: str = None
@@ -47,33 +58,23 @@ class ClientContext:
Initialize the context manager with the key-value pairs to be set.
"""
# Validate the data with the Pydantic model
self.values = ContextModel(**kwargs).dict()
self.tokens = {}
self.values = model_to_dict_without_defaults(ContextModel(**kwargs))
def __enter__(self):
"""
Set the key-value pairs to the context variable `context_data` when entering the context.
"""
# Get the current context data
data = context_data.get()
# For each key-value pair, save the current value of the key (if it exists) and set the new value
for key, value in self.values.items():
self.tokens[key] = data.get(key, None)
data[key] = value
data = deepcopy(context_data.get()) if context_data.get() else {}
data.update(self.values)
# Update the context data
context_data.set(data)
self.token = context_data.set(data)
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Reset the context variable `context_data` to its previous values when exiting the context.
"""
# Get the current context data
data = context_data.get()
# For each key, if a previous value exists, reset it. Otherwise, remove the key
for key in self.values.keys():
if self.tokens[key] is not None:
data[key] = self.tokens[key]
else:
data.pop(key, None)
# Update the context data
context_data.set(data)
context_data.reset(self.token)

View File

@@ -491,10 +491,15 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
"chat_prompt_size": self.max_token_length,
}
config.update(PRESET_SIMPLE_1)
config.update(PRESET_LLAMA_PRECISE)
return config
def prompt_config_analyze_freeform_short(self, prompt: str) -> dict:
config = self.prompt_config_analyze_freeform(prompt)
config["max_new_tokens"] = 10
return config
def prompt_config_narrate(self, prompt: str) -> dict:
prompt = self.prompt_template(
system_prompts.NARRATOR,
@@ -606,7 +611,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
fn_prompt_config = getattr(self, f"prompt_config_{kind}")
fn_url = self.prompt_url
message = fn_prompt_config(prompt)
if client_context_attribute("nuke_repetition") > 0.0:
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"))
@@ -621,6 +626,8 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
log.debug("send_prompt", token_length=token_length, max_token_length=self.max_token_length)
message["prompt"] = message["prompt"].strip()
#print(f"prompt: |{message['prompt']}|")
response = await self.send_message(message, fn_url())

View File

@@ -22,6 +22,7 @@ from .cmd_save import CmdSave
from .cmd_save_as import CmdSaveAs
from .cmd_save_characters import CmdSaveCharacters
from .cmd_setenv import CmdSetEnvironmentToScene, CmdSetEnvironmentToCreative
from .cmd_time_util import *
from .cmd_world_state import CmdWorldState
from .cmd_run_helios_test import CmdHeliosTest
from .manager import Manager

View File

@@ -20,8 +20,11 @@ class CmdRebuildArchive(TalemateCommand):
if not summarizer:
self.system_message("No summarizer found")
return True
self.scene.archived_history = []
# clear out archived history, but keep pre-established history
self.scene.archived_history = [
ah for ah in self.scene.archived_history if ah.get("end") is None
]
while True:
more = await summarizer.agent.build_archive(self.scene)

View File

@@ -0,0 +1,50 @@
"""
Commands to manage scene timescale
"""
import asyncio
import logging
from talemate.commands.base import TalemateCommand
from talemate.commands.manager import register
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 isodate
__all__ = [
"CmdAdvanceTime",
]
@register
class CmdAdvanceTime(TalemateCommand):
"""
Command class for the 'advance_time' command
"""
name = "advance_time"
description = "Advance the scene time by a given amount (expects iso8601 duration))"
aliases = ["time_a"]
async def run(self):
if not self.args:
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()

View File

@@ -1,8 +1,12 @@
from dataclasses import dataclass
__all__ = [
"ArchiveEntry",
]
@dataclass
class ArchiveEntry:
text: str
start: int
end: int
start: int = None
end: int = None
ts: str = None

View File

@@ -29,6 +29,7 @@ class AbortCommand(IOError):
class Emission:
typ: str
message: str = None
message_object: SceneMessage = None
character: Character = None
scene: Scene = None
status: str = None
@@ -43,12 +44,16 @@ def emit(
if typ not in handlers:
raise ValueError(f"Unknown message type: {typ}")
if isinstance(message, SceneMessage):
kwargs["id"] = message.id
message_object = message
message = message.message
else:
message_object = None
handlers[typ].send(
Emission(typ=typ, message=message, character=character, scene=scene, **kwargs)
Emission(typ=typ, message=message, character=character, scene=scene, message_object=message_object, **kwargs)
)

View File

@@ -5,6 +5,7 @@ NarratorMessage = signal("narrator")
CharacterMessage = signal("character")
PlayerMessage = signal("player")
DirectorMessage = signal("director")
TimePassageMessage = signal("time")
ClearScreen = signal("clear_screen")
@@ -31,6 +32,7 @@ handlers = {
"character": CharacterMessage,
"player": PlayerMessage,
"director": DirectorMessage,
"time": TimePassageMessage,
"request_input": RequestInput,
"receive_input": ReceiveInput,
"client_status": ClientStatus,

View File

@@ -27,6 +27,7 @@ class HistoryEvent(Event):
class ArchiveEvent(Event):
text: str
memory_id: str = None
ts: str = None
@dataclass

View File

@@ -6,7 +6,9 @@ from dotenv import load_dotenv
import talemate.events as events
from talemate import Actor, Character, Player
from talemate.config import load_config
from talemate.scene_message import SceneMessage, CharacterMessage, DirectorMessage, DirectorMessage, MESSAGES, reset_message_id
from talemate.scene_message import (
SceneMessage, CharacterMessage, NarratorMessage, DirectorMessage, MESSAGES, reset_message_id
)
from talemate.world_state import WorldState
import talemate.instance as instance
@@ -144,12 +146,20 @@ async def load_scene_from_data(
)
scene.assets.cover_image = scene_data.get("assets", {}).get("cover_image", None)
scene.assets.load_assets(scene_data.get("assets", {}).get("assets", {}))
scene.sync_time()
log.debug("scene time", ts=scene.ts)
for ah in scene.archived_history:
if reset:
break
ts = ah.get("ts", "PT1S")
if not ah.get("ts"):
ah["ts"] = ts
scene.signals["archive_add"].send(
events.ArchiveEvent(scene=scene, event_type="archive_add", text=ah["text"])
events.ArchiveEvent(scene=scene, event_type="archive_add", text=ah["text"], ts=ts)
)
for character_name, cs in scene.character_states.items():
@@ -312,7 +322,7 @@ def _prepare_legacy_history(entry):
"""
if entry.startswith("*"):
cls = DirectorMessage
cls = NarratorMessage
elif entry.startswith("Director instructs"):
cls = DirectorMessage
else:

View File

@@ -177,6 +177,9 @@ class Prompt:
# prompt variables
vars: dict = dataclasses.field(default_factory=dict)
# pad prepared response and ai response with a white-space
pad_prepended_response: bool = True
prepared_response: str = ""
eval_response: bool = False
@@ -282,6 +285,7 @@ class Prompt:
env.globals["set_question_eval"] = self.set_question_eval
env.globals["query_scene"] = self.query_scene
env.globals["query_memory"] = self.query_memory
env.globals["query_text"] = self.query_text
env.globals["uuidgen"] = lambda: str(uuid.uuid4())
env.globals["to_int"] = lambda x: int(x)
env.globals["config"] = self.config
@@ -336,7 +340,15 @@ class Prompt:
f"Answer: " + loop.run_until_complete(narrator.narrate_query(query, at_the_end=at_the_end, as_narrative=as_narrative)),
])
def query_text(self, query:str, text:str):
loop = asyncio.get_event_loop()
summarizer = instance.get_agent("summarizer")
query = query.format(**self.vars)
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):
loop = asyncio.get_event_loop()
@@ -524,7 +536,8 @@ class Prompt:
response = await client.send_prompt(str(self), kind=kind)
if not response.lower().startswith(self.prepared_response.lower()):
response = self.prepared_response.rstrip() + " " + response.strip()
pad = " " if self.pad_prepended_response else ""
response = self.prepared_response.rstrip() + pad + response.strip()
if self.eval_response:

View File

@@ -7,14 +7,14 @@
{% for character in characters -%}
{{ character.name }}:
{{ character.filtered_sheet(['name', 'description', 'age', 'gender']) }}
{{ query_memory(character.name+' personality', as_question_answer= False) }}
{{ query_memory("what is "+character.name+"'s personality?", as_question_answer=False) }}
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:DIALOGUE EXAMPLES|>
{% for dialogue in talking_character.example_dialogue -%}
{{ dialogue }}
{% endfor -%}
{% for example in talking_character.random_dialogue_examples(num=3) -%}
{{ example }}
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:TASK|>

View File

@@ -0,0 +1,6 @@
{{ dialogue }}
<|SECTION:TASK|>
Examine the dialogue from the beginning and find the last line that marks a scene change. Repeat the line back to me exactly as it is written.
<|CLOSE_SECTION|>
{{ bot_token }}The first line that marks a scene change is:

View File

@@ -0,0 +1,7 @@
{{ text }}
<|SECTION:TASK|>
Analyze the text above and answer the question.
Question: {{ query }}
{{ bot_token }}Answer:

View File

@@ -0,0 +1,5 @@
<|SECTION:SCENE|>
{{ text }}
<|SECTION:TASK|>
Question: How much time has passed in the scene above?
{{ bot_token }}Answer (ISO8601 duration): P

View File

@@ -5,6 +5,9 @@
<|SECTION:TASK|>
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.
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,4 +1,5 @@
from dataclasses import dataclass, field
import isodate
_message_id = 0
@@ -11,11 +12,23 @@ def reset_message_id():
global _message_id
_message_id = 0
@dataclass
class SceneMessage:
"""
Base class for all messages that are sent to the scene.
"""
# the mesage itself
message: str
# the id of the message
id: int = field(default_factory=get_message_id)
# the source of the message (e.g. "ai", "progress_story", "director")
source: str = ""
typ = "scene"
@@ -83,12 +96,25 @@ class DirectorMessage(SceneMessage):
return f"[Story progression instructions for {char_name}: {message}]"
@dataclass
class TimePassageMessage(SceneMessage):
ts: str = "PT0S"
source: str = "manual"
typ = "time"
def __dict__(self):
return {
"message": self.message,
"id": self.id,
"typ": "time",
"source": self.source,
"ts": self.ts,
}
MESSAGES = {
"scene": SceneMessage,
"character": CharacterMessage,
"narrator": NarratorMessage,
"director": DirectorMessage,
"time": TimePassageMessage,
}

View File

@@ -292,6 +292,16 @@ class WebsocketHandler(Receiver):
}
)
def handle_time(self, emission: Emission):
self.queue_put(
{
"type": "time",
"message": emission.message,
"id": emission.id,
"ts": emission.message_object.ts,
}
)
def handle_prompt_sent(self, emission: Emission):
self.queue_put(
{

View File

@@ -5,6 +5,7 @@ import os
import random
import traceback
import re
import isodate
from typing import Dict, List, Optional, Union
from blinker import signal
@@ -18,7 +19,7 @@ import talemate.util as util
import talemate.save as save
from talemate.emit import Emitter, emit, wait_for_input
from talemate.util import colored_text, count_tokens, extract_metadata, wrap_text
from talemate.scene_message import SceneMessage, CharacterMessage, DirectorMessage, NarratorMessage
from talemate.scene_message import SceneMessage, CharacterMessage, DirectorMessage, NarratorMessage, TimePassageMessage
from talemate.exceptions import ExitScene, RestartSceneLoop, ResetScene, TalemateError, TalemateInterrupt, LLMAccuracyError
from talemate.world_state import WorldState
from talemate.config import SceneConfig
@@ -141,6 +142,29 @@ class Character:
return random.choice(self.example_dialogue)
def random_dialogue_examples(self, num:int=3):
"""
Get multiple random example dialogue lines for this character.
Will return up to `num` examples and not have any duplicates.
"""
if not self.example_dialogue:
return []
# create copy of example_dialogue so we dont modify the original
examples = self.example_dialogue.copy()
# shuffle the examples so we get a random order
random.shuffle(examples)
# now pop examples until we have `num` examples or we run out of examples
return [examples.pop() for _ in range(min(num, len(examples)))]
def filtered_sheet(self, attributes: list[str]):
"""
@@ -260,10 +284,11 @@ class Character:
if "color" in metadata:
self.color = metadata["color"]
if "mes_example" in metadata:
new_line_match = "\r\n" if "\r\n" in metadata["mes_example"] else "\n"
for message in metadata["mes_example"].split("<START>"):
if message.strip("\r\n"):
if message.strip(new_line_match):
self.example_dialogue.extend(
[m for m in message.split("\r\n") if m]
[m for m in message.split(new_line_match) if m]
)
@@ -335,9 +360,9 @@ class Character:
}
})
for detail in self.details:
for key, detail in self.details.items():
items.append({
"text": f"{self.name} details: {detail}",
"text": f"{self.name} - {key}: {detail}",
"meta": {
"character": self.name,
"typ": "details",
@@ -522,6 +547,7 @@ class Scene(Emitter):
self.environment = "scene"
self.goal = None
self.world_state = WorldState()
self.ts = "PT0S"
self.automated_actions = {}
@@ -610,6 +636,10 @@ class Scene(Emitter):
def push_history(self, messages: list[SceneMessage]):
"""
Adds one or more messages to the scene history
"""
if isinstance(messages, SceneMessage):
messages = [messages]
@@ -623,6 +653,9 @@ class Scene(Emitter):
if isinstance(self.history[idx], DirectorMessage):
self.history.pop(idx)
break
elif isinstance(message, TimePassageMessage):
self.advance_time(message.ts)
self.history.extend(messages)
self.signals["history_add"].send(
@@ -634,19 +667,26 @@ class Scene(Emitter):
)
def push_archive(self, entry: data_objects.ArchiveEntry):
"""
Adds an entry to the archive history.
The archive history is a list of summarized history entries.
"""
self.archived_history.append(entry.__dict__)
self.signals["archive_add"].send(
events.ArchiveEvent(
scene=self,
event_type="archive_add",
text=entry.text,
ts=entry.ts,
)
)
emit("archived_history", data={
"history":[archived_history["text"] for archived_history in self.archived_history]
})
def edit_message(self, message_id:int, message:str):
"""
Finds the message in `history` by its id and will update its contents
@@ -828,7 +868,7 @@ class Scene(Emitter):
# we then take the history from the end index to the end of the history
if self.archived_history:
end = self.archived_history[-1]["end"]
end = self.archived_history[-1].get("end", 0)
else:
end = 0
@@ -865,8 +905,17 @@ class Scene(Emitter):
# description at tbe beginning of the context history
archive_insert_idx = 0
# iterate backwards through archived history and count how many entries
# there are that have an end index
num_archived_entries = 0
if add_archieved_history:
for i in range(len(self.archived_history) - 1, -1, -1):
if self.archived_history[i].get("end") is None:
break
num_archived_entries += 1
if len(self.archived_history) <= 2 and add_archieved_history:
if num_archived_entries <= 2 and add_archieved_history:
for character in self.characters:
@@ -898,6 +947,13 @@ class Scene(Emitter):
context_history.insert(archive_insert_idx, "<|CLOSE_SECTION|>")
while i >= 0 and limit > 0 and add_archieved_history:
# we skip predefined history, that should be joined in through
# long term memory queries
if self.archived_history[i].get("end") is None:
break
text = self.archived_history[i]["text"]
if count_tokens(context_history) + count_tokens(text) > budget:
break
@@ -1038,6 +1094,11 @@ class Scene(Emitter):
self.history.pop(i)
log.info(f"Deleted message {message_id}")
emit("remove_message", "", id=message_id)
if isinstance(message, TimePassageMessage):
self.sync_time()
self.emit_status()
break
def emit_status(self):
@@ -1050,6 +1111,7 @@ class Scene(Emitter):
"scene_config": self.scene_config,
"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,
},
)
@@ -1059,7 +1121,58 @@ class Scene(Emitter):
"""
self.environment = environment
self.emit_status()
def advance_time(self, ts: str):
"""
Accepts an iso6801 duration string and advances the scene's world state by that amount
"""
self.ts = isodate.duration_isoformat(
isodate.parse_duration(self.ts) + isodate.parse_duration(ts)
)
def sync_time(self):
"""
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"
for message in self.history:
if isinstance(message, TimePassageMessage):
self.advance_time(message.ts)
self.log.info("sync_time", ts=self.ts)
# TODO: need to adjust archived_history ts as well
# but removal also probably means the history needs to be regenerated
# anyway.
def calc_time(self, start_idx:int=0, end_idx:int=None):
"""
Loops through self.history looking for TimePassageMessage and will
return the sum iso8601 duration string
Defines start and end indexes
"""
ts = "PT0S"
found = False
for message in self.history[start_idx:end_idx]:
if isinstance(message, TimePassageMessage):
util.iso8601_add(ts, message.ts)
found = True
if not found:
return None
return ts
async def start(self):
"""
Start the scene
@@ -1117,7 +1230,7 @@ class Scene(Emitter):
actor = self.get_character(char_name).actor
except AttributeError:
# If the character is not an actor, then it is the narrator
self.narrator_message(item)
emit(item.typ, item)
continue
emit("character", item, character=actor.character)
if not actor.character.is_player:
@@ -1249,6 +1362,7 @@ class Scene(Emitter):
"context": scene.context,
"world_state": scene.world_state.dict(),
"assets": scene.assets.dict(),
"ts": scene.ts,
}
emit("system", "Saving scene data to: " + filepath)

View File

@@ -4,6 +4,8 @@ import json
import re
import textwrap
import structlog
import isodate
import datetime
from typing import List
from colorama import Back, Fore, Style, init
@@ -297,6 +299,26 @@ def pronouns(gender: str) -> tuple[str, str]:
return (pronoun, possessive_determiner)
def strip_partial_sentences(text:str) -> str:
# Sentence ending characters
sentence_endings = ['.', '!', '?', '"', "*"]
# Check if the last character is already a sentence ending
if text[-1] in sentence_endings:
return text
# Split the text into words
words = text.split()
# Iterate over the words in reverse order until a sentence ending is found
for i in range(len(words) - 1, -1, -1):
if words[i][-1] in sentence_endings:
return ' '.join(words[:i+1])
# If no sentence ending is found, return the original text
return text
def clean_paragraph(paragraph: str) -> str:
"""
Cleans up a paragraph of text by:
@@ -431,4 +453,145 @@ def fix_faulty_json(data: str) -> str:
data = re.sub(r',\s*}', '}', data)
data = re.sub(r',\s*]', ']', data)
return data
return data
def duration_to_timedelta(duration):
"""Convert an isodate.Duration object to a datetime.timedelta object."""
days = int(duration.years) * 365 + int(duration.months) * 30 + int(duration.days)
return datetime.timedelta(days=days)
def timedelta_to_duration(delta):
"""Convert a datetime.timedelta object to an isodate.Duration object."""
days = delta.days
years = days // 365
days %= 365
months = days // 30
days %= 30
return isodate.duration.Duration(years=years, months=months, days=days)
def parse_duration_to_isodate_duration(duration_str):
"""Parse ISO 8601 duration string and ensure the result is an isodate.Duration."""
parsed_duration = isodate.parse_duration(duration_str)
if isinstance(parsed_duration, datetime.timedelta):
days = parsed_duration.days
years = days // 365
days %= 365
months = days // 30
days %= 30
return isodate.duration.Duration(years=years, months=months, days=days)
return parsed_duration
def iso8601_diff(duration_str1, duration_str2):
# Parse the ISO 8601 duration strings ensuring they are isodate.Duration objects
duration1 = parse_duration_to_isodate_duration(duration_str1)
duration2 = parse_duration_to_isodate_duration(duration_str2)
# Convert to timedelta
timedelta1 = duration_to_timedelta(duration1)
timedelta2 = duration_to_timedelta(duration2)
# Calculate the difference
difference_timedelta = abs(timedelta1 - timedelta2)
# Convert back to Duration for further processing
difference = timedelta_to_duration(difference_timedelta)
return difference
def iso8601_duration_to_human(iso_duration, suffix:str=" ago"):
# Parse the ISO8601 duration string into an isodate duration object
if isinstance(iso_duration, isodate.Duration):
duration = iso_duration
else:
duration = isodate.parse_duration(iso_duration)
if isinstance(duration, isodate.Duration):
years = duration.years
months = duration.months
days = duration.days
seconds = duration.tdelta.total_seconds()
else:
years, months = 0, 0
days = duration.days
seconds = duration.total_seconds() - days * 86400 # Extract time-only part
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
components = []
if years:
components.append(f"{years} Year{'s' if years > 1 else ''}")
if months:
components.append(f"{months} Month{'s' if months > 1 else ''}")
if days:
components.append(f"{days} Day{'s' if days > 1 else ''}")
if hours:
components.append(f"{int(hours)} Hour{'s' if hours > 1 else ''}")
if minutes:
components.append(f"{int(minutes)} Minute{'s' if minutes > 1 else ''}")
if seconds:
components.append(f"{int(seconds)} Second{'s' if seconds > 1 else ''}")
# Construct the human-readable string
if len(components) > 1:
last = components.pop()
human_str = ', '.join(components) + ' and ' + last
elif components:
human_str = components[0]
else:
human_str = "0 Seconds"
return f"{human_str}{suffix}"
def iso8601_diff_to_human(start, end):
if not start or not end:
return ""
diff = iso8601_diff(start, end)
return iso8601_duration_to_human(diff)
def iso8601_add(date_a:str, date_b:str) -> str:
"""
Adds two ISO 8601 durations together.
"""
# Validate input
if not date_a or not date_b:
return "PT0S"
new_ts = isodate.parse_duration(date_a.strip()) + isodate.parse_duration(date_b.strip())
return isodate.duration_isoformat(new_ts)
def iso8601_correct_duration(duration: str) -> str:
# Split the string into date and time components using 'T' as the delimiter
parts = duration.split("T")
# Handle the date component
date_component = parts[0]
time_component = ""
# If there's a time component, process it
if len(parts) > 1:
time_component = parts[1]
# Check if the time component has any date values (Y, M, D) and move them to the date component
for char in "YD": # Removed 'M' from this loop
if char in time_component:
index = time_component.index(char)
date_component += time_component[:index+1]
time_component = time_component[index+1:]
# If the date component contains any time values (H, M, S), move them to the time component
for char in "HMS":
if char in date_component:
index = date_component.index(char)
time_component = date_component[index:] + time_component
date_component = date_component[:index]
# Combine the corrected date and time components
corrected_duration = date_component
if time_component:
corrected_duration += "T" + time_component
return corrected_duration

View File

@@ -40,6 +40,11 @@
<DirectorMessage :text="message.text" :message_id="message.id" :character="message.character" />
</div>
</div>
<div v-else-if="message.type === 'time'" :class="`message ${message.type}`">
<div class="time-message" :id="`message-${message.id}`">
<TimePassageMessage :text="message.text" :message_id="message.id" :ts="message.ts" />
</div>
</div>
<div v-else :class="`message ${message.type}`">
{{ message.text }}
</div>
@@ -51,6 +56,7 @@
import CharacterMessage from './CharacterMessage.vue';
import NarratorMessage from './NarratorMessage.vue';
import DirectorMessage from './DirectorMessage.vue';
import TimePassageMessage from './TimePassageMessage.vue';
export default {
name: 'SceneMessages',
@@ -58,6 +64,7 @@ export default {
CharacterMessage,
NarratorMessage,
DirectorMessage,
TimePassageMessage,
},
data() {
return {
@@ -87,6 +94,7 @@ export default {
multiSelect: data.data.multi_select,
color: data.color,
sent: false,
ts: data.ts,
};
this.messages.push(message);
},
@@ -163,10 +171,12 @@ export default {
if (data.message) {
if (data.type === 'character') {
const [character, text] = data.message.split(':');
const parts = data.message.split(':');
const character = parts.shift();
const text = parts.join(':');
this.messages.push({ id: data.id, type: data.type, character: character.trim(), text: text.trim(), color: data.color }); // Add color property to the message
} else if (data.type != 'request_input' && data.type != 'client_status' && data.type != 'agent_status') {
this.messages.push({ id: data.id, type: data.type, text: data.message, color: data.color, character: data.character, status:data.status }); // Add color property to the message
this.messages.push({ id: data.id, type: data.type, text: data.message, color: data.color, character: data.character, status:data.status, ts:data.ts }); // Add color property to the message
}
}

View File

@@ -86,6 +86,20 @@
</v-btn>
</template>
</v-tooltip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-3" v-bind="props" :disabled="isInputDisabled()" color="primary" icon>
<v-icon>mdi-clock</v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader>Advance Time</v-list-subheader>
<v-list-item v-for="(option, index) in advanceTimeOptions" :key="index"
@click="sendHotButtonMessage('!advance_time:' + option.value)">
<v-list-item-title>{{ option.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-divider vertical></v-divider>
<v-tooltip :disabled="isInputDisabled()" location="top" text="Direct a character">
<template v-slot:activator="{ props }">
@@ -142,6 +156,7 @@
</v-card>
</div>
</template>
@@ -154,6 +169,23 @@ export default {
return {
commandActive: false,
commandName: null,
advanceTimeOptions: [
{"value" : "P10Y", "title": "10 years"},
{"value" : "P5Y", "title": "5 years"},
{"value" : "P1Y", "title": "1 year"},
{"value" : "P6M", "title": "6 months"},
{"value" : "P3M", "title": "3 months"},
{"value" : "P1M", "title": "1 month"},
{"value" : "P7D:1 Week later", "title": "1 week"},
{"value" : "P3D", "title": "3 days"},
{"value" : "P1D", "title": "1 day"},
{"value" : "PT8H", "title": "8 hours"},
{"value" : "PT4H", "title": "4 hours"},
{"Value" : "PT1H", "title": "1 hour"},
{"value" : "PT30M", "title": "30 minutes"},
{"value" : "PT15M", "title": "15 minutes"}
],
}
},
inject: [

View File

@@ -97,6 +97,11 @@
<v-btn v-if="scene.environment === 'scene'" class="ml-1" @click="openSceneHistory()"><v-icon size="14"
class="mr-1">mdi-playlist-star</v-icon>History</v-btn>
<v-chip size="x-small" v-if="scene.scene_time !== undefined">
<v-icon>mdi-clock</v-icon>
{{ scene.scene_time }}
</v-chip>
</v-toolbar-title>
<v-toolbar-title v-else>
Talemate
@@ -294,6 +299,7 @@ export default {
this.scene = {
name: data.name,
environment: data.data.environment,
scene_time: data.data.scene_time,
}
this.sceneActive = true;
return;

View File

@@ -0,0 +1,61 @@
<template>
<div class="time-container" v-if="show && minimized" >
<v-chip closable @click:close="deleteMessage()" color="deep-purple-lighten-3">
<v-icon class="mr-2">mdi-clock-outline</v-icon>
<span>{{ text }}</span>
</v-chip>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
minimized: true
}
},
props: ['text', 'message_id', 'ts'],
inject: ['requestDeleteMessage'],
methods: {
toggle() {
this.minimized = !this.minimized;
},
deleteMessage() {
console.log('deleteMessage', this.message_id);
this.requestDeleteMessage(this.message_id);
}
}
}
</script>
<style scoped>
.highlight {
color: #9FA8DA;
font-style: italic;
margin-left: 2px;
margin-right: 2px;
}
.highlight:before {
--content: "*";
}
.highlight:after {
--content: "*";
}
.time-text {
color: #9FA8DA;
}
.time-message {
display: flex;
flex-direction: row;
color: #9FA8DA;
}
.time-container {
}
</style>

View File

@@ -1,2 +1,2 @@
SYSTEM: {{ system_message }}
USER: {{ set_response(prompt, "\nASSISTANT:") }}
USER: {{ set_response(prompt, "\nASSISTANT: ") }}

View File

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

View File

@@ -1,2 +1,2 @@
SYSTEM: {{ system_message }}
USER: {{ set_response(prompt, "\nASSISTANT:") }}
USER: {{ set_response(prompt, "\nASSISTANT: ") }}

View File

@@ -1 +1,2 @@
{{ system_message }} USER: {{ set_response(prompt, " ASSISTANT:") }}
{{ system_message }}
USER: {{ set_response(prompt, "\nASSISTANT: ") }}