feat: Add condensation configuration with provider/model/rotation support (v0.3.2)

- Add 'condensation' section to providers.json for dedicated provider/model
- Support rotation-based condensation by specifying rotation ID in model field
- Update ContextManager to use dedicated condensation handler
- Update handlers to pass condensation configuration
- Bump version to 0.3.2
parent d9772366
......@@ -650,18 +650,11 @@ For support and questions:
The extension includes multiple donation options to support its development:
### Web3/MetaMask Donation
Works on any website - The Web3 donation is completely independent of the current page
Click the "🦊 Donate with MetaMask" button in the extension popup (only appears if MetaMask is detected)
Supports both modern window.ethereum and legacy window.web3 providers
Default donation: 0.1 ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
Users can modify the amount in MetaMask before confirming
### Ethereum Donation
ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
### PayPal Donation
Click the "💳 Donate with PayPal" button in the extension popup
Opens PayPal donation page for info@nexlab.net
Traditional payment method for users without cryptocurrency wallets
Always available regardless of browser setup
https://paypal.me/nexlab
### Bitcoin Donation
Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u
......
......@@ -141,18 +141,11 @@ See `config/providers.json` and `config/rotations.json` for configuration exampl
## Donations
The project includes multiple donation options to support its development:
### Web3/MetaMask Donation
Works on any website - The Web3 donation is completely independent of the current page
Click the "🦊 Donate with MetaMask" button in the extension popup (only appears if MetaMask is detected)
Supports both modern window.ethereum and legacy window.web3 providers
Default donation: 0.1 ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
Users can modify the amount in MetaMask before confirming
### Ethereum Donation
ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
### PayPal Donation
Click the "💳 Donate with PayPal" button in the extension popup
Opens PayPal donation page for info@nexlab.net
Traditional payment method for users without cryptocurrency wallets
Always available regardless of browser setup
https://paypal.me/nexlab
### Bitcoin Donation
Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u
......@@ -162,4 +155,4 @@ Traditional BTC donation method
See `DOCUMENTATION.md` for complete API documentation, configuration details, and development guides.
## License
GNU General Public License v3.0
\ No newline at end of file
GNU General Public License v3.0
......@@ -23,7 +23,7 @@ Why did the programmer quit his job? Because he didn't get arrays!
A modular proxy server for managing multiple AI provider integrations.
"""
from .config import config, Config, ProviderConfig, RotationConfig, AppConfig, AutoselectConfig, AutoselectModelInfo
from .config import config, Config, ProviderConfig, RotationConfig, AppConfig, AutoselectConfig, AutoselectModelInfo, CondensationConfig
from .context import ContextManager, get_context_config_for_model
from .database import DatabaseManager, get_database, initialize_database
from .models import (
......@@ -46,7 +46,7 @@ from .providers import (
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.3.0"
__version__ = "0.3.2"
__all__ = [
# Config
"config",
......
......@@ -35,6 +35,14 @@ class ProviderModelConfig(BaseModel):
max_request_tokens: Optional[int] = None
class CondensationConfig(BaseModel):
"""Configuration for context condensation"""
provider_id: Optional[str] = None
model: Optional[str] = None
rotation_id: Optional[str] = None
enabled: bool = True
class ProviderConfig(BaseModel):
id: str
name: str
......@@ -63,6 +71,7 @@ class AppConfig(BaseModel):
providers: Dict[str, ProviderConfig]
rotations: Dict[str, RotationConfig]
autoselect: Dict[str, AutoselectConfig]
condensation: Optional[CondensationConfig] = None
error_tracking: Dict[str, Dict]
class Config:
......@@ -71,6 +80,7 @@ class Config:
self._load_providers()
self._load_rotations()
self._load_autoselect()
self._load_condensation()
self._initialize_error_tracking()
def _get_config_source_dir(self):
......@@ -208,6 +218,30 @@ class Config:
with open(autoselect_path) as f:
data = json.load(f)
self.autoselect = {k: AutoselectConfig(**v) for k, v in data.items()}
def _load_condensation(self):
"""Load condensation configuration from providers.json"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== Config._load_condensation START ===")
providers_path = Path.home() / '.aisbf' / 'providers.json'
if not providers_path.exists():
# Fallback to source config if user config doesn't exist
try:
source_dir = self._get_config_source_dir()
providers_path = source_dir / 'providers.json'
except FileNotFoundError:
logger.warning("Could not find providers.json for condensation config")
self.condensation = CondensationConfig()
return
with open(providers_path) as f:
data = json.load(f)
condensation_data = data.get('condensation', {})
self.condensation = CondensationConfig(**condensation_data)
logger.info(f"Loaded condensation config: provider_id={self.condensation.provider_id}, model={self.condensation.model}, enabled={self.condensation.enabled}")
logger.info(f"=== Config._load_condensation END ===")
def _initialize_error_tracking(self):
self.error_tracking = {}
......@@ -235,5 +269,8 @@ class Config:
def get_autoselect(self, autoselect_id: str) -> AutoselectConfig:
return self.autoselect.get(autoselect_id)
def get_condensation(self) -> CondensationConfig:
return self.condensation
config = Config()
......@@ -25,6 +25,9 @@ Context management and condensation for AISBF.
import logging
from typing import Dict, List, Optional, Union, Any
from .utils import count_messages_tokens
from .config import config
from .providers import get_provider_handler
from .handlers import RotationHandler
class ContextManager:
......@@ -32,18 +35,70 @@ class ContextManager:
Manages context size and performs condensation when needed.
"""
def __init__(self, model_config: Dict, provider_handler=None):
def __init__(self, model_config: Dict, provider_handler=None, condensation_config=None):
"""
Initialize the context manager.
Args:
model_config: Model configuration dictionary containing context_size, condense_context, condense_method
provider_handler: Optional provider handler for making summarization requests
provider_handler: Optional provider handler for making summarization requests (fallback)
condensation_config: Optional condensation configuration for dedicated provider/model/rotation
"""
self.context_size = model_config.get('context_size')
self.condense_context = model_config.get('condense_context', 0)
self.condense_method = model_config.get('condense_method')
self.provider_handler = provider_handler
self.condensation_config = condensation_config or config.get_condensation()
# Initialize condensation provider handler if configured
self.condensation_handler = None
self.condensation_model = None
self._rotation_handler = None
self._rotation_id = None
if (self.condensation_config and
self.condensation_config.enabled):
try:
# Check if model is a rotation ID or direct model name
model_value = self.condensation_config.model
# Check if this model value is a rotation ID (exists in rotations config)
is_rotation = False
if model_value:
try:
rotation_config = config.get_rotation(model_value)
if rotation_config:
is_rotation = True
logger = logging.getLogger(__name__)
logger.info(f"Condensation model '{model_value}' is a rotation ID")
except:
pass # Not a rotation, treat as direct model
if is_rotation:
# Use rotation handler for condensation
rotation_handler = RotationHandler()
# Store rotation handler and rotation_id for later use
self._rotation_handler = rotation_handler
self._rotation_id = model_value
# The actual model will be selected by rotation handler
self.condensation_model = None # Will be determined by rotation
logger = logging.getLogger(__name__)
logger.info(f"Initialized condensation with rotation: rotation_id={model_value}")
elif self.condensation_config.provider_id and model_value:
# Use provider handler for condensation with direct model
provider_config = config.get_provider(self.condensation_config.provider_id)
if provider_config:
api_key = provider_config.api_key
self.condensation_handler = get_provider_handler(
self.condensation_config.provider_id,
api_key
)
self.condensation_model = model_value
logger = logging.getLogger(__name__)
logger.info(f"Initialized condensation handler: provider={self.condensation_config.provider_id}, model={model_value}")
except Exception as e:
logger = logging.getLogger(__name__)
logger.warning(f"Failed to initialize condensation handler: {e}")
# Normalize condense_context to 0-100 range
if self.condense_context and self.condense_context > 100:
......@@ -198,10 +253,15 @@ class ContextManager:
logger = logging.getLogger(__name__)
logger.info(f"Conversational condensation: {len(messages)} messages")
if not self.provider_handler:
# Use dedicated condensation handler if available, otherwise fallback to provider_handler
handler = self.condensation_handler if self.condensation_handler else self.provider_handler
if not handler:
logger.warning("No provider handler available for conversational condensation, skipping")
return messages
# Use dedicated condensation model if configured, otherwise use same model
condense_model = self.condensation_model if self.condensation_model else model
if len(messages) <= 4:
# Not enough messages to condense
return messages
......@@ -227,36 +287,68 @@ class ContextManager:
summary_prompt += f"{role}: {content}\n"
try:
# Request summary from the model
summary_messages = [{"role": "user", "content": summary_prompt}]
summary_response = await self.provider_handler.handle_request(
model=model,
messages=summary_messages,
max_tokens=1000,
temperature=0.3,
stream=False
)
# Extract summary content
if isinstance(summary_response, dict):
summary_content = summary_response.get('choices', [{}])[0].get('message', {}).get('content', '')
if summary_content:
# Create summary message
summary_message = {
"role": "system",
"content": f"[CONVERSATION SUMMARY]\n{summary_content}"
}
# Build condensed messages: system + summary + recent
condensed = system_messages + [summary_message] + recent_messages
# If using rotation handler, call rotation handler's method
if self._rotation_handler and not self.condensation_model:
# Create a minimal request for condensation
condensation_request = {
"messages": [{"role": "user", "content": summary_prompt}],
"temperature": 0.3,
"max_tokens": 1000,
"stream": False
}
# Call rotation handler to get condensation
response = await self._rotation_handler.handle_rotation_request(self._rotation_id, condensation_request)
# Extract summary content
if isinstance(response, dict):
summary_content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
# Update stored summary
self.conversation_summary = summary_content
self.summary_token_count = count_messages_tokens([summary_message], model)
if summary_content:
# Create summary message
summary_message = {
"role": "system",
"content": f"[CONVERSATION SUMMARY]\n{summary_content}"
}
# Build condensed messages: system + summary + recent
condensed = system_messages + [summary_message] + recent_messages
# Update stored summary
self.conversation_summary = summary_content
self.summary_token_count = count_messages_tokens([summary_message], model)
logger.info(f"Conversational: Created summary via rotation ({len(summary_content)} chars)")
return condensed
else:
# Request summary from the model directly
summary_messages = [{"role": "user", "content": summary_prompt}]
summary_response = await handler.handle_request(
model=condense_model,
messages=summary_messages,
max_tokens=1000,
temperature=0.3,
stream=False
)
# Extract summary content
if isinstance(summary_response, dict):
summary_content = summary_response.get('choices', [{}])[0].get('message', {}).get('content', '')
logger.info(f"Conversational: Created summary ({len(summary_content)} chars)")
return condensed
if summary_content:
# Create summary message
summary_message = {
"role": "system",
"content": f"[CONVERSATION SUMMARY]\n{summary_content}"
}
# Build condensed messages: system + summary + recent
condensed = system_messages + [summary_message] + recent_messages
# Update stored summary
self.conversation_summary = summary_content
self.summary_token_count = count_messages_tokens([summary_message], model)
logger.info(f"Conversational: Created summary ({len(summary_content)} chars)")
return condensed
except Exception as e:
logger.error(f"Error during conversational condensation: {e}")
......@@ -278,10 +370,15 @@ class ContextManager:
logger = logging.getLogger(__name__)
logger.info(f"Semantic condensation: {len(messages)} messages")
if not self.provider_handler:
# Use dedicated condensation handler if available, otherwise fallback to provider_handler
handler = self.condensation_handler if self.condensation_handler else self.provider_handler
if not handler:
logger.warning("No provider handler available for semantic condensation, skipping")
return messages
# Use dedicated condensation model if configured, otherwise use same model
condense_model = self.condensation_model if self.condensation_model else model
if len(messages) <= 2:
return messages
......@@ -321,33 +418,65 @@ Conversation History:
Provide only the relevant information in a concise format."""
try:
# Request pruned context from the model
prune_messages = [{"role": "user", "content": prune_prompt}]
prune_response = await self.provider_handler.handle_request(
model=model,
messages=prune_messages,
max_tokens=2000,
temperature=0.2,
stream=False
)
# Extract pruned content
if isinstance(prune_response, dict):
pruned_content = prune_response.get('choices', [{}])[0].get('message', {}).get('content', '')
# If using rotation handler, call rotation handler's method
if self._rotation_handler and not self.condensation_model:
# Create a minimal request for condensation
condensation_request = {
"messages": [{"role": "user", "content": prune_prompt}],
"temperature": 0.2,
"max_tokens": 2000,
"stream": False
}
# Call rotation handler to get condensation
response = await self._rotation_handler.handle_rotation_request(self._rotation_id, condensation_request)
# Extract pruned content
if isinstance(response, dict):
pruned_content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
if pruned_content:
# Create pruned context message
pruned_message = {
"role": "system",
"content": f"[RELEVANT CONTEXT]\n{pruned_content}"
}
# Build condensed messages: system + pruned + last user message
last_message = messages[-1] if messages else None
if last_message and last_message.get('role') != 'system':
condensed = system_messages + [pruned_message, last_message]
else:
condensed = system_messages + [pruned_message]
logger.info(f"Semantic: Pruned to relevant context via rotation ({len(pruned_content)} chars)")
return condensed
else:
# Request pruned context from the model directly
prune_messages = [{"role": "user", "content": prune_prompt}]
prune_response = await handler.handle_request(
model=condense_model,
messages=prune_messages,
max_tokens=2000,
temperature=0.2,
stream=False
)
if pruned_content:
# Create pruned context message
pruned_message = {
"role": "system",
"content": f"[RELEVANT CONTEXT]\n{pruned_content}"
}
# Extract pruned content
if isinstance(prune_response, dict):
pruned_content = prune_response.get('choices', [{}])[0].get('message', {}).get('content', '')
# Build condensed messages: system + pruned + last user message
last_message = messages[-1] if messages else None
if last_message and last_message.get('role') != 'system':
condensed = system_messages + [pruned_message, last_message]
else:
condensed = system_messages + [pruned_message]
if pruned_content:
# Create pruned context message
pruned_message = {
"role": "system",
"content": f"[RELEVANT CONTEXT]\n{pruned_content}"
}
# Build condensed messages: system + pruned + last user message
last_message = messages[-1] if messages else None
if last_message and last_message.get('role') != 'system':
condensed = system_messages + [pruned_message, last_message]
else:
condensed = system_messages + [pruned_message]
logger.info(f"Semantic: Pruned to relevant context ({len(pruned_content)} chars)")
return condensed
......
......@@ -263,7 +263,7 @@ class RequestHandler:
# Apply context condensation if needed
if context_config.get('condense_context', 0) > 0:
context_manager = ContextManager(context_config, handler)
context_manager = ContextManager(context_config, handler, self.config.get_condensation())
if context_manager.should_condense(messages, model):
logger.info("Context condensation triggered")
messages = await context_manager.condense_context(messages, model)
......@@ -369,7 +369,7 @@ class RequestHandler:
# Apply context condensation if needed
if context_config.get('condense_context', 0) > 0:
context_manager = ContextManager(context_config, handler)
context_manager = ContextManager(context_config, handler, self.config.get_condensation())
if context_manager.should_condense(messages, model):
import logging
logger = logging.getLogger(__name__)
......@@ -1017,7 +1017,7 @@ class RotationHandler:
# Apply context condensation if needed
if context_config.get('condense_context', 0) > 0:
context_manager = ContextManager(context_config, handler)
context_manager = ContextManager(context_config, handler, self.config.get_condensation())
if context_manager.should_condense(messages, model_name):
logger.info("Context condensation triggered")
messages = await context_manager.condense_context(messages, model_name)
......
{
"condensation": {
"provider_id": "gemini",
"model": "gemini-1.5-flash",
"enabled": true
},
"providers": {
"gemini": {
"id": "gemini",
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.3.0"
version = "0.3.2"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.3.0",
version="0.3.2",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment