Add send_image tool, fix Qwen no-user-query errors, prevent model contention
- Add send_image tool: fetches image from URL and posts it inline in Discord chat - Harden _ensure_user_tail: inject/fix user message whenever last message is not a non-empty user message (catches empty content from stripped thinking tags) - Fix life sim: fallback chain for empty thinking-tag-stripped responses so user messages are never empty; add _ensure_user_tail before each LLM call - Add _agent_active flag: background tasks skip when run_agent is active, preventing model contention when two different models are configured - Add reload_personality tool, fetch_url improvements, misc system prompt updates - Persist channel_history.json via docker-compose volume Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0f0856501
commit
d53835bb5f
2 files changed
+157
-16
No files matched your search
@@ -16,6 +16,8 @@ logging.basicConfig(
|
|||||||
log = logging.getLogger("dadbot")
|
log = logging.getLogger("dadbot")
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
import httpx
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from discord.ext import tasks
|
from discord.ext import tasks
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -97,6 +99,18 @@ UPDATE_REFLECTION_NOTES_TOOL = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RELOAD_PERSONALITY_TOOL = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "reload_personality",
|
||||||
|
"description": (
|
||||||
|
"Reload your personality from disk. Use this if you suspect your in-memory personality "
|
||||||
|
"is out of date or you've been told your personality was updated externally."
|
||||||
|
),
|
||||||
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
READ_LIFE_HISTORY_TOOL = {
|
READ_LIFE_HISTORY_TOOL = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -147,7 +161,38 @@ FETCH_URL_TOOL = {
|
|||||||
"url": {
|
"url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The URL to fetch.",
|
"description": "The URL to fetch.",
|
||||||
}
|
},
|
||||||
|
"max_chars": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum characters to return (default 20000). Increase if the page is long and you need more content.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SEND_IMAGE_TOOL = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "send_image",
|
||||||
|
"description": (
|
||||||
|
"Fetch an image from the internet and post it directly into the Discord chat. "
|
||||||
|
"Use this when asked to find and share an image, photo, meme, or picture. "
|
||||||
|
"The image will appear inline in the channel. Pair it with a caption if you want to say something about it."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Direct URL to the image file (jpg, png, gif, webp, etc.).",
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional caption or comment to send alongside the image.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["url"],
|
"required": ["url"],
|
||||||
},
|
},
|
||||||
@@ -318,10 +363,12 @@ LIFE_SIM_LOG_FILE = Path(os.getenv("LIFE_SIM_LOG_FILE", "life_sim_log.txt"))
|
|||||||
LIFE_SIM_MAX_TURNS = int(os.getenv("LIFE_SIM_MAX_TURNS", "6"))
|
LIFE_SIM_MAX_TURNS = int(os.getenv("LIFE_SIM_MAX_TURNS", "6"))
|
||||||
_last_message_time: float = 0.0
|
_last_message_time: float = 0.0
|
||||||
_life_sim_turn: bool = False
|
_life_sim_turn: bool = False
|
||||||
|
reflections_enabled: bool = True
|
||||||
|
_agent_active: bool = False # True while run_agent is handling a user message
|
||||||
|
|
||||||
|
|
||||||
def background_on_cooldown() -> bool:
|
def background_on_cooldown() -> bool:
|
||||||
return (time.monotonic() - _last_background_error) < BACKGROUND_COOLDOWN
|
return _agent_active or (time.monotonic() - _last_background_error) < BACKGROUND_COOLDOWN
|
||||||
|
|
||||||
|
|
||||||
def record_background_error():
|
def record_background_error():
|
||||||
@@ -579,9 +626,9 @@ async def call_mcp_tool(name: str, args: dict) -> str:
|
|||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
async def fetch_url(url: str, max_chars: int = 8000) -> str:
|
async def fetch_url(url: str, max_chars: int = 20000) -> str:
|
||||||
import httpx
|
log.info(f"[fetch_url] Fetching {url}")
|
||||||
from bs4 import BeautifulSoup
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
|
async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
|
||||||
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
@@ -598,14 +645,49 @@ async def fetch_url(url: str, max_chars: int = 8000) -> str:
|
|||||||
result = "\n".join(lines)
|
result = "\n".join(lines)
|
||||||
if len(result) > max_chars:
|
if len(result) > max_chars:
|
||||||
result = result[:max_chars] + "\n\n[truncated]"
|
result = result[:max_chars] + "\n\n[truncated]"
|
||||||
|
log.info(f"[fetch_url] Done in {time.monotonic() - t0:.1f}s — {len(result)} chars")
|
||||||
return result or "Page returned no readable text."
|
return result or "Page returned no readable text."
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
log.warning(f"[fetch_url] Failed: {exc}")
|
||||||
return f"Failed to fetch URL: {exc}"
|
return f"Failed to fetch URL: {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
async def send_image_to_channel(url: str, channel: discord.abc.Messageable, caption: str | None = None) -> str:
|
||||||
|
log.info(f"[send_image] Fetching {url}")
|
||||||
|
if not url.startswith(("http://", "https://")):
|
||||||
|
return "Error: only http/https URLs are supported."
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
|
||||||
|
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
r.raise_for_status()
|
||||||
|
ct = r.headers.get("content-type", "")
|
||||||
|
if not ct.startswith("image/"):
|
||||||
|
return f"Error: URL did not return an image (content-type: {ct})."
|
||||||
|
data = r.content
|
||||||
|
# Derive a filename from the URL
|
||||||
|
filename = url.split("?")[0].rstrip("/").split("/")[-1] or "image"
|
||||||
|
if "." not in filename:
|
||||||
|
ext = ct.split("/")[-1].split(";")[0]
|
||||||
|
filename = f"{filename}.{ext}"
|
||||||
|
file = discord.File(fp=__import__("io").BytesIO(data), filename=filename)
|
||||||
|
await channel.send(content=caption or "", file=file)
|
||||||
|
log.info(f"[send_image] Sent {filename} ({len(data)} bytes)")
|
||||||
|
return f"Image sent successfully ({len(data)} bytes)."
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning(f"[send_image] Failed: {exc}")
|
||||||
|
return f"Failed to fetch or send image: {exc}"
|
||||||
|
|
||||||
|
|
||||||
def _ensure_user_tail(messages: list) -> None:
|
def _ensure_user_tail(messages: list) -> None:
|
||||||
"""Qwen's chat template requires the last message to be from the user. Inject a continuation prompt if needed."""
|
"""Qwen's chat template requires a non-empty user message as the last message."""
|
||||||
if messages and messages[-1]["role"] not in ("user", "system"):
|
if not messages:
|
||||||
|
return
|
||||||
|
last = messages[-1]
|
||||||
|
if last["role"] == "user" and last.get("content", "").strip():
|
||||||
|
return # already a valid user tail
|
||||||
|
if last["role"] == "user":
|
||||||
|
last["content"] = "Please continue." # user message exists but content is empty
|
||||||
|
else:
|
||||||
messages.append({"role": "user", "content": "Please continue."})
|
messages.append({"role": "user", "content": "Please continue."})
|
||||||
|
|
||||||
|
|
||||||
@@ -628,9 +710,18 @@ async def fetch_discord_history(channel: discord.abc.Messageable, limit: int = 5
|
|||||||
|
|
||||||
async def run_agent(messages: list, channel: discord.abc.Messageable | None = None) -> str:
|
async def run_agent(messages: list, channel: discord.abc.Messageable | None = None) -> str:
|
||||||
"""Call the LLM in a loop, executing tool calls until a final reply is produced."""
|
"""Call the LLM in a loop, executing tool calls until a final reply is produced."""
|
||||||
all_tools = openai_tools + [PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL, READ_LIFE_HISTORY_TOOL, FETCH_URL_TOOL]
|
global _agent_active
|
||||||
|
_agent_active = True
|
||||||
|
try:
|
||||||
|
return await _run_agent_inner(messages, channel)
|
||||||
|
finally:
|
||||||
|
_agent_active = False
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_agent_inner(messages: list, channel: discord.abc.Messageable | None = None) -> str:
|
||||||
|
all_tools = openai_tools + [PERSONALITY_TOOL, RELOAD_PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL, READ_LIFE_HISTORY_TOOL, FETCH_URL_TOOL]
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
all_tools = all_tools + [FETCH_CHANNEL_HISTORY_TOOL]
|
all_tools = all_tools + [FETCH_CHANNEL_HISTORY_TOOL, SEND_IMAGE_TOOL]
|
||||||
kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools, "tool_choice": "required"}
|
kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools, "tool_choice": "required"}
|
||||||
|
|
||||||
iteration = 0
|
iteration = 0
|
||||||
@@ -666,8 +757,11 @@ async def run_agent(messages: list, channel: discord.abc.Messageable | None = No
|
|||||||
log.info(f"Tool call: {tc.function.name}({args})")
|
log.info(f"Tool call: {tc.function.name}({args})")
|
||||||
|
|
||||||
if tc.function.name == "update_personality":
|
if tc.function.name == "update_personality":
|
||||||
save_personality(args["personality"])
|
save_personality(args["personality"], reasoning="(direct chat)")
|
||||||
result = "Personality updated successfully."
|
result = "Personality updated successfully."
|
||||||
|
elif tc.function.name == "reload_personality":
|
||||||
|
load_personality()
|
||||||
|
result = f"Personality reloaded from disk ({len(personality_notes)} chars)."
|
||||||
elif tc.function.name == "update_reflection_notes":
|
elif tc.function.name == "update_reflection_notes":
|
||||||
save_reflection_notes(args["notes"])
|
save_reflection_notes(args["notes"])
|
||||||
result = "Reflection notes updated."
|
result = "Reflection notes updated."
|
||||||
@@ -680,7 +774,12 @@ async def run_agent(messages: list, channel: discord.abc.Messageable | None = No
|
|||||||
else:
|
else:
|
||||||
result = "Channel history is not available in this context."
|
result = "Channel history is not available in this context."
|
||||||
elif tc.function.name == "fetch_url":
|
elif tc.function.name == "fetch_url":
|
||||||
result = await fetch_url(args["url"])
|
result = await fetch_url(args["url"], max_chars=args.get("max_chars", 20000))
|
||||||
|
elif tc.function.name == "send_image":
|
||||||
|
if channel is not None:
|
||||||
|
result = await send_image_to_channel(args["url"], channel, caption=args.get("caption"))
|
||||||
|
else:
|
||||||
|
result = "send_image is not available in this context."
|
||||||
else:
|
else:
|
||||||
result = await call_mcp_tool(tc.function.name, args)
|
result = await call_mcp_tool(tc.function.name, args)
|
||||||
|
|
||||||
@@ -795,6 +894,9 @@ async def run_life_event_reflection(messages: list):
|
|||||||
|
|
||||||
async def run_life_sim_session():
|
async def run_life_sim_session():
|
||||||
"""Background coroutine: run one life simulation event as a two-LLM dialogue."""
|
"""Background coroutine: run one life simulation event as a two-LLM dialogue."""
|
||||||
|
if not reflections_enabled:
|
||||||
|
log.info("[life-sim] Skipping — reflections disabled")
|
||||||
|
return
|
||||||
if background_on_cooldown():
|
if background_on_cooldown():
|
||||||
log.info("[life-sim] Skipping — model on cooldown")
|
log.info("[life-sim] Skipping — model on cooldown")
|
||||||
return
|
return
|
||||||
@@ -826,7 +928,10 @@ async def run_life_sim_session():
|
|||||||
if personality_notes:
|
if personality_notes:
|
||||||
dadbot_system_lines += ["", "Your personality:", personality_notes]
|
dadbot_system_lines += ["", "Your personality:", personality_notes]
|
||||||
|
|
||||||
lm_messages: list[dict] = [{"role": "system", "content": lm_system}]
|
lm_messages: list[dict] = [
|
||||||
|
{"role": "system", "content": lm_system},
|
||||||
|
{"role": "user", "content": "Begin the event."},
|
||||||
|
]
|
||||||
dadbot_messages: list[dict] = [{"role": "system", "content": "\n".join(dadbot_system_lines)}]
|
dadbot_messages: list[dict] = [{"role": "system", "content": "\n".join(dadbot_system_lines)}]
|
||||||
transcript_parts: list[str] = []
|
transcript_parts: list[str] = []
|
||||||
|
|
||||||
@@ -834,6 +939,7 @@ async def run_life_sim_session():
|
|||||||
log.info(f"[life-sim] Turn {turn + 1}/{LIFE_SIM_MAX_TURNS} — Life Master")
|
log.info(f"[life-sim] Turn {turn + 1}/{LIFE_SIM_MAX_TURNS} — Life Master")
|
||||||
|
|
||||||
# Life Master narrates
|
# Life Master narrates
|
||||||
|
_ensure_user_tail(lm_messages)
|
||||||
lm_response = await asyncio.wait_for(
|
lm_response = await asyncio.wait_for(
|
||||||
llm.chat.completions.create(
|
llm.chat.completions.create(
|
||||||
model=BACKGROUND_MODEL,
|
model=BACKGROUND_MODEL,
|
||||||
@@ -841,7 +947,8 @@ async def run_life_sim_session():
|
|||||||
),
|
),
|
||||||
timeout=BACKGROUND_LLM_TIMEOUT,
|
timeout=BACKGROUND_LLM_TIMEOUT,
|
||||||
)
|
)
|
||||||
lm_text = strip_thinking_tags(lm_response.choices[0].message.content.strip())
|
raw_lm = lm_response.choices[0].message.content or ""
|
||||||
|
lm_text = strip_thinking_tags(raw_lm.strip()) or raw_lm.strip() or "[...]"
|
||||||
transcript_parts.append(f"[Life Master turn {turn + 1}]\n{lm_text}")
|
transcript_parts.append(f"[Life Master turn {turn + 1}]\n{lm_text}")
|
||||||
log.info(f"[life-sim] Life Master: {lm_text[:100]}")
|
log.info(f"[life-sim] Life Master: {lm_text[:100]}")
|
||||||
|
|
||||||
@@ -856,6 +963,7 @@ async def run_life_sim_session():
|
|||||||
log.info(f"[life-sim] Turn {turn + 1}/{LIFE_SIM_MAX_TURNS} — DadBot")
|
log.info(f"[life-sim] Turn {turn + 1}/{LIFE_SIM_MAX_TURNS} — DadBot")
|
||||||
|
|
||||||
# DadBot responds
|
# DadBot responds
|
||||||
|
_ensure_user_tail(dadbot_messages)
|
||||||
dadbot_response = await asyncio.wait_for(
|
dadbot_response = await asyncio.wait_for(
|
||||||
llm.chat.completions.create(
|
llm.chat.completions.create(
|
||||||
model=BACKGROUND_MODEL,
|
model=BACKGROUND_MODEL,
|
||||||
@@ -863,7 +971,8 @@ async def run_life_sim_session():
|
|||||||
),
|
),
|
||||||
timeout=BACKGROUND_LLM_TIMEOUT,
|
timeout=BACKGROUND_LLM_TIMEOUT,
|
||||||
)
|
)
|
||||||
dadbot_text = strip_thinking_tags(dadbot_response.choices[0].message.content.strip())
|
raw_dadbot = dadbot_response.choices[0].message.content or ""
|
||||||
|
dadbot_text = strip_thinking_tags(raw_dadbot.strip()) or raw_dadbot.strip() or "[...]"
|
||||||
transcript_parts.append(f"[DadBot turn {turn + 1}]\n{dadbot_text}")
|
transcript_parts.append(f"[DadBot turn {turn + 1}]\n{dadbot_text}")
|
||||||
log.info(f"[life-sim] DadBot: {dadbot_text[:100]}")
|
log.info(f"[life-sim] DadBot: {dadbot_text[:100]}")
|
||||||
|
|
||||||
@@ -880,6 +989,7 @@ async def run_life_sim_session():
|
|||||||
"Be specific and personal."
|
"Be specific and personal."
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
_ensure_user_tail(lm_messages)
|
||||||
summary_response = await asyncio.wait_for(
|
summary_response = await asyncio.wait_for(
|
||||||
llm.chat.completions.create(
|
llm.chat.completions.create(
|
||||||
model=BACKGROUND_MODEL,
|
model=BACKGROUND_MODEL,
|
||||||
@@ -922,6 +1032,9 @@ async def run_life_sim_session():
|
|||||||
|
|
||||||
async def reflect_on_self():
|
async def reflect_on_self():
|
||||||
"""Background coroutine: run a private self-reflection session when idle."""
|
"""Background coroutine: run a private self-reflection session when idle."""
|
||||||
|
if not reflections_enabled:
|
||||||
|
log.info("[reflection] Skipping — reflections disabled")
|
||||||
|
return
|
||||||
if background_on_cooldown():
|
if background_on_cooldown():
|
||||||
log.info("[reflection] Skipping — model on cooldown")
|
log.info("[reflection] Skipping — model on cooldown")
|
||||||
return
|
return
|
||||||
@@ -1037,9 +1150,33 @@ async def on_ready():
|
|||||||
async def on_message(message: discord.Message):
|
async def on_message(message: discord.Message):
|
||||||
if message.author.bot:
|
if message.author.bot:
|
||||||
return
|
return
|
||||||
global _last_message_time
|
global _last_message_time, reflections_enabled
|
||||||
_last_message_time = time.monotonic()
|
_last_message_time = time.monotonic()
|
||||||
|
|
||||||
|
# Admin commands
|
||||||
|
cmd = message.content.strip().lower()
|
||||||
|
if cmd == "!reflect on":
|
||||||
|
if reflections_enabled:
|
||||||
|
await message.reply("Reflections are already on.")
|
||||||
|
else:
|
||||||
|
reflections_enabled = True
|
||||||
|
if not reflection_loop.is_running():
|
||||||
|
reflection_loop.start()
|
||||||
|
await message.reply("Reflections enabled.")
|
||||||
|
return
|
||||||
|
if cmd == "!reflect off":
|
||||||
|
if not reflections_enabled:
|
||||||
|
await message.reply("Reflections are already off.")
|
||||||
|
else:
|
||||||
|
reflections_enabled = False
|
||||||
|
reflection_loop.stop()
|
||||||
|
await message.reply("Reflections disabled.")
|
||||||
|
return
|
||||||
|
if cmd == "!reflect":
|
||||||
|
status = "on" if reflections_enabled else "off"
|
||||||
|
await message.reply(f"Reflections are currently **{status}**. Use `!reflect on` or `!reflect off` to toggle.")
|
||||||
|
return
|
||||||
|
|
||||||
history = channel_history[message.channel.id]
|
history = channel_history[message.channel.id]
|
||||||
|
|
||||||
content = message.content
|
content = message.content
|
||||||
@@ -1083,7 +1220,10 @@ async def on_message(message: discord.Message):
|
|||||||
system_content = f"{system_content}\n\nYour current personality notes (you wrote these yourself and can update them anytime using the update_personality tool):\n{personality_notes}"
|
system_content = f"{system_content}\n\nYour current personality notes (you wrote these yourself and can update them anytime using the update_personality tool):\n{personality_notes}"
|
||||||
system_content = f"{system_content}\n\nYou have an update_reflection_notes tool. Call it whenever a conversation surfaces something worth remembering for later — an interesting topic, a question you didn't get to explore, something a user mentioned that you want to look into, or a thought you want to develop. Don't wait to be asked; if something catches your attention, note it down before responding. You review these notes privately between conversations."
|
system_content = f"{system_content}\n\nYou have an update_reflection_notes tool. Call it whenever a conversation surfaces something worth remembering for later — an interesting topic, a question you didn't get to explore, something a user mentioned that you want to look into, or a thought you want to develop. Don't wait to be asked; if something catches your attention, note it down before responding. You review these notes privately between conversations."
|
||||||
system_content = f"{system_content}\n\nYou have a read_life_history tool. Use it to recall the simulated life events you've lived through — your early memories, experiences, and how they've shaped you. Call it when someone asks about your past, your childhood, or your personal experiences."
|
system_content = f"{system_content}\n\nYou have a read_life_history tool. Use it to recall the simulated life events you've lived through — your early memories, experiences, and how they've shaped you. Call it when someone asks about your past, your childhood, or your personal experiences."
|
||||||
system_content = f"{system_content}\n\nYou have a fetch_channel_history tool. Use it when you need more context than what's already in your conversation window — for example when you've just been mentioned for the first time, when someone references something said earlier, or when you're unsure what's been going on in the channel."
|
system_content = f"{system_content}\n\nYou have a fetch_url tool. Use it whenever someone shares a link and wants you to read, summarise, or discuss the contents of a webpage."
|
||||||
|
system_content = f"{system_content}\n\nYou have a send_image tool. Use it when asked to find or share an image, photo, meme, or picture — fetch the image from a direct URL and it will appear inline in the chat. You can include an optional caption."
|
||||||
|
system_content = f"{system_content}\n\nYou have a fetch_channel_history tool. Use it when you need more context than what's already in your conversation window — for example when you've just been mentioned for the first time, when someone references something said earlier, or when you're unsure what's been going on in the channel. If someone tells you something was said or decided in the chat and you don't have it in your context, fetch the channel history before saying you don't know."
|
||||||
|
system_content = f"{system_content}\n\nYou have a reload_personality tool. Use it if someone tells you your personality was updated externally or if you suspect your current personality notes are stale."
|
||||||
system_content = f"{system_content}\n\n{date_context}"
|
system_content = f"{system_content}\n\n{date_context}"
|
||||||
if dossier_context:
|
if dossier_context:
|
||||||
system_content = f"{system_content}\n\n{dossier_context}"
|
system_content = f"{system_content}\n\n{dossier_context}"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ services:
|
|||||||
- ./used_life_events.json:/app/used_life_events.json
|
- ./used_life_events.json:/app/used_life_events.json
|
||||||
- ./life_history.txt:/app/life_history.txt
|
- ./life_history.txt:/app/life_history.txt
|
||||||
- ./life_sim_log.txt:/app/life_sim_log.txt
|
- ./life_sim_log.txt:/app/life_sim_log.txt
|
||||||
|
- ./channel_history.json:/app/channel_history.json
|
||||||
networks:
|
networks:
|
||||||
- litellm_net
|
- litellm_net
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user