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: ...@@ -650,18 +650,11 @@ For support and questions:
The extension includes multiple donation options to support its development: The extension includes multiple donation options to support its development:
### Web3/MetaMask Donation ### Ethereum Donation
Works on any website - The Web3 donation is completely independent of the current page ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
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
### PayPal Donation ### PayPal Donation
Click the "💳 Donate with PayPal" button in the extension popup https://paypal.me/nexlab
Opens PayPal donation page for info@nexlab.net
Traditional payment method for users without cryptocurrency wallets
Always available regardless of browser setup
### Bitcoin Donation ### Bitcoin Donation
Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u
......
...@@ -141,18 +141,11 @@ See `config/providers.json` and `config/rotations.json` for configuration exampl ...@@ -141,18 +141,11 @@ See `config/providers.json` and `config/rotations.json` for configuration exampl
## Donations ## Donations
The project includes multiple donation options to support its development: The project includes multiple donation options to support its development:
### Web3/MetaMask Donation ### Ethereum Donation
Works on any website - The Web3 donation is completely independent of the current page ETH to 0xdA6dAb526515b5cb556d20269207D43fcc760E51
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
### PayPal Donation ### PayPal Donation
Click the "💳 Donate with PayPal" button in the extension popup https://paypal.me/nexlab
Opens PayPal donation page for info@nexlab.net
Traditional payment method for users without cryptocurrency wallets
Always available regardless of browser setup
### Bitcoin Donation ### Bitcoin Donation
Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u Address: bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u
......
...@@ -23,7 +23,7 @@ Why did the programmer quit his job? Because he didn't get arrays! ...@@ -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. 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 .context import ContextManager, get_context_config_for_model
from .database import DatabaseManager, get_database, initialize_database from .database import DatabaseManager, get_database, initialize_database
from .models import ( from .models import (
...@@ -46,7 +46,7 @@ from .providers import ( ...@@ -46,7 +46,7 @@ from .providers import (
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model 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__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -35,6 +35,14 @@ class ProviderModelConfig(BaseModel): ...@@ -35,6 +35,14 @@ class ProviderModelConfig(BaseModel):
max_request_tokens: Optional[int] = None 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): class ProviderConfig(BaseModel):
id: str id: str
name: str name: str
...@@ -63,6 +71,7 @@ class AppConfig(BaseModel): ...@@ -63,6 +71,7 @@ class AppConfig(BaseModel):
providers: Dict[str, ProviderConfig] providers: Dict[str, ProviderConfig]
rotations: Dict[str, RotationConfig] rotations: Dict[str, RotationConfig]
autoselect: Dict[str, AutoselectConfig] autoselect: Dict[str, AutoselectConfig]
condensation: Optional[CondensationConfig] = None
error_tracking: Dict[str, Dict] error_tracking: Dict[str, Dict]
class Config: class Config:
...@@ -71,6 +80,7 @@ class Config: ...@@ -71,6 +80,7 @@ class Config:
self._load_providers() self._load_providers()
self._load_rotations() self._load_rotations()
self._load_autoselect() self._load_autoselect()
self._load_condensation()
self._initialize_error_tracking() self._initialize_error_tracking()
def _get_config_source_dir(self): def _get_config_source_dir(self):
...@@ -209,6 +219,30 @@ class Config: ...@@ -209,6 +219,30 @@ class Config:
data = json.load(f) data = json.load(f)
self.autoselect = {k: AutoselectConfig(**v) for k, v in data.items()} 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): def _initialize_error_tracking(self):
self.error_tracking = {} self.error_tracking = {}
for provider_id in self.providers: for provider_id in self.providers:
...@@ -236,4 +270,7 @@ class Config: ...@@ -236,4 +270,7 @@ class Config:
def get_autoselect(self, autoselect_id: str) -> AutoselectConfig: def get_autoselect(self, autoselect_id: str) -> AutoselectConfig:
return self.autoselect.get(autoselect_id) return self.autoselect.get(autoselect_id)
def get_condensation(self) -> CondensationConfig:
return self.condensation
config = Config() config = Config()
...@@ -25,6 +25,9 @@ Context management and condensation for AISBF. ...@@ -25,6 +25,9 @@ Context management and condensation for AISBF.
import logging import logging
from typing import Dict, List, Optional, Union, Any from typing import Dict, List, Optional, Union, Any
from .utils import count_messages_tokens from .utils import count_messages_tokens
from .config import config
from .providers import get_provider_handler
from .handlers import RotationHandler
class ContextManager: class ContextManager:
...@@ -32,18 +35,70 @@ class ContextManager: ...@@ -32,18 +35,70 @@ class ContextManager:
Manages context size and performs condensation when needed. 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. Initialize the context manager.
Args: Args:
model_config: Model configuration dictionary containing context_size, condense_context, condense_method 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.context_size = model_config.get('context_size')
self.condense_context = model_config.get('condense_context', 0) self.condense_context = model_config.get('condense_context', 0)
self.condense_method = model_config.get('condense_method') self.condense_method = model_config.get('condense_method')
self.provider_handler = provider_handler 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 # Normalize condense_context to 0-100 range
if self.condense_context and self.condense_context > 100: if self.condense_context and self.condense_context > 100:
...@@ -198,10 +253,15 @@ class ContextManager: ...@@ -198,10 +253,15 @@ class ContextManager:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"Conversational condensation: {len(messages)} messages") 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") logger.warning("No provider handler available for conversational condensation, skipping")
return messages 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: if len(messages) <= 4:
# Not enough messages to condense # Not enough messages to condense
return messages return messages
...@@ -227,10 +287,42 @@ class ContextManager: ...@@ -227,10 +287,42 @@ class ContextManager:
summary_prompt += f"{role}: {content}\n" summary_prompt += f"{role}: {content}\n"
try: try:
# Request summary from the model # 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', '')
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_messages = [{"role": "user", "content": summary_prompt}]
summary_response = await self.provider_handler.handle_request( summary_response = await handler.handle_request(
model=model, model=condense_model,
messages=summary_messages, messages=summary_messages,
max_tokens=1000, max_tokens=1000,
temperature=0.3, temperature=0.3,
...@@ -278,10 +370,15 @@ class ContextManager: ...@@ -278,10 +370,15 @@ class ContextManager:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"Semantic condensation: {len(messages)} messages") 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") logger.warning("No provider handler available for semantic condensation, skipping")
return messages 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: if len(messages) <= 2:
return messages return messages
...@@ -321,10 +418,42 @@ Conversation History: ...@@ -321,10 +418,42 @@ Conversation History:
Provide only the relevant information in a concise format.""" Provide only the relevant information in a concise format."""
try: try:
# Request pruned context from the model # 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_messages = [{"role": "user", "content": prune_prompt}]
prune_response = await self.provider_handler.handle_request( prune_response = await handler.handle_request(
model=model, model=condense_model,
messages=prune_messages, messages=prune_messages,
max_tokens=2000, max_tokens=2000,
temperature=0.2, temperature=0.2,
......
...@@ -263,7 +263,7 @@ class RequestHandler: ...@@ -263,7 +263,7 @@ class RequestHandler:
# Apply context condensation if needed # Apply context condensation if needed
if context_config.get('condense_context', 0) > 0: 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): if context_manager.should_condense(messages, model):
logger.info("Context condensation triggered") logger.info("Context condensation triggered")
messages = await context_manager.condense_context(messages, model) messages = await context_manager.condense_context(messages, model)
...@@ -369,7 +369,7 @@ class RequestHandler: ...@@ -369,7 +369,7 @@ class RequestHandler:
# Apply context condensation if needed # Apply context condensation if needed
if context_config.get('condense_context', 0) > 0: 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): if context_manager.should_condense(messages, model):
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -1017,7 +1017,7 @@ class RotationHandler: ...@@ -1017,7 +1017,7 @@ class RotationHandler:
# Apply context condensation if needed # Apply context condensation if needed
if context_config.get('condense_context', 0) > 0: 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): if context_manager.should_condense(messages, model_name):
logger.info("Context condensation triggered") logger.info("Context condensation triggered")
messages = await context_manager.condense_context(messages, model_name) messages = await context_manager.condense_context(messages, model_name)
......
{ {
"condensation": {
"provider_id": "gemini",
"model": "gemini-1.5-flash",
"enabled": true
},
"providers": { "providers": {
"gemini": { "gemini": {
"id": "gemini", "id": "gemini",
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" 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" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.3.0", version="0.3.2",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", 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", 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