Make conversation timeout configuration (so that you're able to resume a discussion manually with another model with a gap of several hours in case you stepped away)
This commit is contained in:
@@ -86,6 +86,12 @@ DEFAULT_THINKING_MODE_THINKDEEP=high
|
|||||||
# The Redis URL for conversation threading - typically managed by docker-compose
|
# The Redis URL for conversation threading - typically managed by docker-compose
|
||||||
# REDIS_URL=redis://redis:6379/0
|
# REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# Optional: Conversation timeout (hours)
|
||||||
|
# How long AI-to-AI conversation threads persist before expiring
|
||||||
|
# Longer timeouts use more Redis memory but allow resuming conversations later
|
||||||
|
# Defaults to 3 hours if not specified
|
||||||
|
CONVERSATION_TIMEOUT_HOURS=3
|
||||||
|
|
||||||
# Optional: Logging level (DEBUG, INFO, WARNING, ERROR)
|
# Optional: Logging level (DEBUG, INFO, WARNING, ERROR)
|
||||||
# DEBUG: Shows detailed operational messages for troubleshooting (default)
|
# DEBUG: Shows detailed operational messages for troubleshooting (default)
|
||||||
# INFO: Shows general operational messages
|
# INFO: Shows general operational messages
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ This server enables **true AI collaboration** between Claude and multiple AI mod
|
|||||||
- **Asynchronous workflow**: Conversations don't need to be sequential - Claude can work on tasks between exchanges, then return to Gemini with additional context and progress updates
|
- **Asynchronous workflow**: Conversations don't need to be sequential - Claude can work on tasks between exchanges, then return to Gemini with additional context and progress updates
|
||||||
- **Incremental updates**: Share only new information in each exchange while maintaining full conversation history
|
- **Incremental updates**: Share only new information in each exchange while maintaining full conversation history
|
||||||
- **Automatic 25K limit bypass**: Each exchange sends only incremental context, allowing unlimited total conversation size
|
- **Automatic 25K limit bypass**: Each exchange sends only incremental context, allowing unlimited total conversation size
|
||||||
- Up to 5 exchanges per conversation with 1-hour expiry
|
- Up to 5 exchanges per conversation with 3-hour expiry (configurable)
|
||||||
- Thread-safe with Redis persistence across all tools
|
- Thread-safe with Redis persistence across all tools
|
||||||
|
|
||||||
**Cross-tool & Cross-Model Continuation Example:**
|
**Cross-tool & Cross-Model Continuation Example:**
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ services:
|
|||||||
- CUSTOM_MODEL_NAME=${CUSTOM_MODEL_NAME:-llama3.2}
|
- CUSTOM_MODEL_NAME=${CUSTOM_MODEL_NAME:-llama3.2}
|
||||||
- DEFAULT_MODEL=${DEFAULT_MODEL:-auto}
|
- DEFAULT_MODEL=${DEFAULT_MODEL:-auto}
|
||||||
- DEFAULT_THINKING_MODE_THINKDEEP=${DEFAULT_THINKING_MODE_THINKDEEP:-high}
|
- DEFAULT_THINKING_MODE_THINKDEEP=${DEFAULT_THINKING_MODE_THINKDEEP:-high}
|
||||||
|
- CONVERSATION_TIMEOUT_HOURS=${CONVERSATION_TIMEOUT_HOURS:-3}
|
||||||
# Model usage restrictions
|
# Model usage restrictions
|
||||||
- OPENAI_ALLOWED_MODELS=${OPENAI_ALLOWED_MODELS:-}
|
- OPENAI_ALLOWED_MODELS=${OPENAI_ALLOWED_MODELS:-}
|
||||||
- GOOGLE_ALLOWED_MODELS=${GOOGLE_ALLOWED_MODELS:-}
|
- GOOGLE_ALLOWED_MODELS=${GOOGLE_ALLOWED_MODELS:-}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import pytest
|
|||||||
|
|
||||||
from server import get_follow_up_instructions
|
from server import get_follow_up_instructions
|
||||||
from utils.conversation_memory import (
|
from utils.conversation_memory import (
|
||||||
|
CONVERSATION_TIMEOUT_SECONDS,
|
||||||
MAX_CONVERSATION_TURNS,
|
MAX_CONVERSATION_TURNS,
|
||||||
ConversationTurn,
|
ConversationTurn,
|
||||||
ThreadContext,
|
ThreadContext,
|
||||||
@@ -40,7 +41,7 @@ class TestConversationMemory:
|
|||||||
mock_client.setex.assert_called_once()
|
mock_client.setex.assert_called_once()
|
||||||
call_args = mock_client.setex.call_args
|
call_args = mock_client.setex.call_args
|
||||||
assert call_args[0][0] == f"thread:{thread_id}" # key
|
assert call_args[0][0] == f"thread:{thread_id}" # key
|
||||||
assert call_args[0][1] == 3600 # TTL
|
assert call_args[0][1] == CONVERSATION_TIMEOUT_SECONDS # TTL from configuration
|
||||||
|
|
||||||
@patch("utils.conversation_memory.get_redis_client")
|
@patch("utils.conversation_memory.get_redis_client")
|
||||||
def test_get_thread_valid(self, mock_redis):
|
def test_get_thread_valid(self, mock_redis):
|
||||||
|
|||||||
@@ -59,6 +59,22 @@ logger = logging.getLogger(__name__)
|
|||||||
# Configuration constants
|
# Configuration constants
|
||||||
MAX_CONVERSATION_TURNS = 10 # Maximum turns allowed per conversation thread
|
MAX_CONVERSATION_TURNS = 10 # Maximum turns allowed per conversation thread
|
||||||
|
|
||||||
|
# Get conversation timeout from environment (in hours), default to 3 hours
|
||||||
|
try:
|
||||||
|
CONVERSATION_TIMEOUT_HOURS = int(os.getenv("CONVERSATION_TIMEOUT_HOURS", "3"))
|
||||||
|
if CONVERSATION_TIMEOUT_HOURS <= 0:
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid CONVERSATION_TIMEOUT_HOURS value ({CONVERSATION_TIMEOUT_HOURS}), using default of 3 hours"
|
||||||
|
)
|
||||||
|
CONVERSATION_TIMEOUT_HOURS = 3
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid CONVERSATION_TIMEOUT_HOURS value ('{os.getenv('CONVERSATION_TIMEOUT_HOURS')}'), using default of 3 hours"
|
||||||
|
)
|
||||||
|
CONVERSATION_TIMEOUT_HOURS = 3
|
||||||
|
|
||||||
|
CONVERSATION_TIMEOUT_SECONDS = CONVERSATION_TIMEOUT_HOURS * 3600
|
||||||
|
|
||||||
|
|
||||||
class ConversationTurn(BaseModel):
|
class ConversationTurn(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -154,7 +170,7 @@ def create_thread(tool_name: str, initial_request: dict[str, Any], parent_thread
|
|||||||
str: UUID thread identifier that can be used for continuation
|
str: UUID thread identifier that can be used for continuation
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
- Thread expires after 1 hour (3600 seconds)
|
- Thread expires after the configured timeout (default: 3 hours)
|
||||||
- Non-serializable parameters are filtered out automatically
|
- Non-serializable parameters are filtered out automatically
|
||||||
- Thread can be continued by any tool using the returned UUID
|
- Thread can be continued by any tool using the returned UUID
|
||||||
- Parent thread creates a chain for conversation history traversal
|
- Parent thread creates a chain for conversation history traversal
|
||||||
@@ -179,10 +195,10 @@ def create_thread(tool_name: str, initial_request: dict[str, Any], parent_thread
|
|||||||
initial_context=filtered_context,
|
initial_context=filtered_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store in Redis with 1 hour TTL to prevent indefinite accumulation
|
# Store in Redis with configurable TTL to prevent indefinite accumulation
|
||||||
client = get_redis_client()
|
client = get_redis_client()
|
||||||
key = f"thread:{thread_id}"
|
key = f"thread:{thread_id}"
|
||||||
client.setex(key, 3600, context.model_dump_json())
|
client.setex(key, CONVERSATION_TIMEOUT_SECONDS, context.model_dump_json())
|
||||||
|
|
||||||
logger.debug(f"[THREAD] Created new thread {thread_id} with parent {parent_thread_id}")
|
logger.debug(f"[THREAD] Created new thread {thread_id} with parent {parent_thread_id}")
|
||||||
|
|
||||||
@@ -261,7 +277,7 @@ def add_turn(
|
|||||||
- Redis connection failure
|
- Redis connection failure
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
- Refreshes thread TTL to 1 hour on successful update
|
- Refreshes thread TTL to configured timeout on successful update
|
||||||
- Turn limits prevent runaway conversations
|
- Turn limits prevent runaway conversations
|
||||||
- File references are preserved for cross-tool access
|
- File references are preserved for cross-tool access
|
||||||
- Model information enables cross-provider conversations
|
- Model information enables cross-provider conversations
|
||||||
@@ -297,7 +313,7 @@ def add_turn(
|
|||||||
try:
|
try:
|
||||||
client = get_redis_client()
|
client = get_redis_client()
|
||||||
key = f"thread:{thread_id}"
|
key = f"thread:{thread_id}"
|
||||||
client.setex(key, 3600, context.model_dump_json()) # Refresh TTL to 1 hour
|
client.setex(key, CONVERSATION_TIMEOUT_SECONDS, context.model_dump_json()) # Refresh TTL to configured timeout
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"[FLOW] Failed to save turn to Redis: {type(e).__name__}")
|
logger.debug(f"[FLOW] Failed to save turn to Redis: {type(e).__name__}")
|
||||||
|
|||||||
Reference in New Issue
Block a user