configuration et al: implemented new Config format

This commit is contained in:
juk0de 2023-09-06 22:52:03 +02:00
parent df19391c54
commit d7055163f9
4 changed files with 108 additions and 32 deletions

View File

@ -33,18 +33,23 @@ class AI(Protocol):
The base class for AI clients. The base class for AI clients.
""" """
ID: str
name: str name: str
config: AIConfig config: AIConfig
def request(self, def request(self,
question: Message, question: Message,
context: Chat, chat: Chat,
num_answers: int = 1, num_answers: int = 1,
otags: Optional[set[Tag]] = None) -> AIResponse: otags: Optional[set[Tag]] = None) -> AIResponse:
""" """
Make an AI request, asking the given question with the given Make an AI request. Parameters:
context (i. e. chat history). The nr. of requested answers * question: the question to ask
corresponds to the nr. of messages in the 'AIResponse'. * chat: the chat history to be added as context
* num_answers: nr. of requested answers (corresponds
to the nr. of messages in the 'AIResponse')
* otags: the output tags, i. e. the tags that all
returned messages should contain
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -3,7 +3,8 @@ Creates different AI instances, based on the given configuration.
""" """
import argparse import argparse
from .configuration import Config from typing import cast
from .configuration import Config, OpenAIConfig, default_ai_ID
from .ai import AI, AIError from .ai import AI, AIError
from .ais.openai import OpenAI from .ais.openai import OpenAI
@ -12,9 +13,14 @@ def create_ai(args: argparse.Namespace, config: Config) -> AI:
""" """
Creates an AI subclass instance from the given args and configuration. Creates an AI subclass instance from the given args and configuration.
""" """
if args.ai == 'openai': if args.ai:
# FIXME: create actual 'OpenAIConfig' and set values from 'args' ai_conf = config.ais[args.ai]
# FIXME: use actual name from config elif default_ai_ID in config.ais:
return OpenAI("openai", config.openai) ai_conf = config.ais[default_ai_ID]
else:
raise AIError("No AI name given and no default exists")
if ai_conf.name == 'openai':
return OpenAI(cast(OpenAIConfig, ai_conf))
else: else:
raise AIError(f"AI '{args.ai}' is not supported") raise AIError(f"AI '{args.ai}' is not supported")

View File

@ -17,9 +17,11 @@ class OpenAI(AI):
The OpenAI AI client. The OpenAI AI client.
""" """
def __init__(self, name: str, config: OpenAIConfig) -> None: def __init__(self, config: OpenAIConfig) -> None:
self.name = name self.ID = config.ID
self.name = config.name
self.config = config self.config = config
openai.api_key = config.api_key
def request(self, def request(self,
question: Message, question: Message,
@ -31,8 +33,7 @@ class OpenAI(AI):
chat history. The nr. of requested answers corresponds to the chat history. The nr. of requested answers corresponds to the
nr. of messages in the 'AIResponse'. nr. of messages in the 'AIResponse'.
""" """
# FIXME: use real 'system' message (store in OpenAIConfig) oai_chat = self.openai_chat(chat, self.config.system, question)
oai_chat = self.openai_chat(chat, "system", question)
response = openai.ChatCompletion.create( response = openai.ChatCompletion.create(
model=self.config.model, model=self.config.model,
messages=oai_chat, messages=oai_chat,

View File

@ -1,16 +1,28 @@
import yaml import yaml
from typing import Type, TypeVar, Any from pathlib import Path
from dataclasses import dataclass, asdict from typing import Type, TypeVar, Any, Optional, Final
from dataclasses import dataclass, asdict, field
ConfigInst = TypeVar('ConfigInst', bound='Config') ConfigInst = TypeVar('ConfigInst', bound='Config')
AIConfigInst = TypeVar('AIConfigInst', bound='AIConfig')
OpenAIConfigInst = TypeVar('OpenAIConfigInst', bound='OpenAIConfig') OpenAIConfigInst = TypeVar('OpenAIConfigInst', bound='OpenAIConfig')
supported_ais: list[str] = ['openai']
default_ai_ID: str = 'default'
default_config_path = '.config.yaml'
class ConfigError(Exception):
pass
@dataclass @dataclass
class AIConfig: class AIConfig:
""" """
The base class of all AI configurations. The base class of all AI configurations.
""" """
ID: str
name: str name: str
@ -19,21 +31,27 @@ class OpenAIConfig(AIConfig):
""" """
The OpenAI section of the configuration file. The OpenAI section of the configuration file.
""" """
api_key: str # all members have default values, so we can easily create
model: str # a default configuration
temperature: float ID: str = 'default'
max_tokens: int api_key: str = '0123456789'
top_p: float system: str = 'You are an assistant'
frequency_penalty: float model: str = 'gpt-3.5-turbo-16k'
presence_penalty: float temperature: float = 1.0
max_tokens: int = 4000
top_p: float = 1.0
frequency_penalty: float = 0.0
presence_penalty: float = 0.0
# the name should not be changed
name: Final[str] = 'openai'
@classmethod @classmethod
def from_dict(cls: Type[OpenAIConfigInst], source: dict[str, Any]) -> OpenAIConfigInst: def from_dict(cls: Type[OpenAIConfigInst], source: dict[str, Any]) -> OpenAIConfigInst:
""" """
Create OpenAIConfig from a dict. Create OpenAIConfig from a dict.
""" """
return cls( res = cls(
name='OpenAI', system=str(source['system']),
api_key=str(source['api_key']), api_key=str(source['api_key']),
model=str(source['model']), model=str(source['model']),
max_tokens=int(source['max_tokens']), max_tokens=int(source['max_tokens']),
@ -42,6 +60,33 @@ class OpenAIConfig(AIConfig):
frequency_penalty=float(source['frequency_penalty']), frequency_penalty=float(source['frequency_penalty']),
presence_penalty=float(source['presence_penalty']) presence_penalty=float(source['presence_penalty'])
) )
# overwrite default ID if provided
if 'ID' in source:
res.ID = source['ID']
return res
def as_dict(self) -> dict[str, Any]:
return asdict(self)
def ai_config_instance(name: str, conf_dict: Optional[dict[str, Any]] = None) -> AIConfig:
"""
Creates an AIConfig instance of the given name.
"""
if name.lower() == 'openai':
if conf_dict is None:
return OpenAIConfig()
else:
return OpenAIConfig.from_dict(conf_dict)
else:
raise ConfigError(f"AI '{name}' is not supported")
def create_default_ai_configs() -> dict[str, AIConfig]:
"""
Create a dict containing default configurations for all supported AIs.
"""
return {ai_config_instance(name).ID: ai_config_instance(name) for name in supported_ais}
@dataclass @dataclass
@ -49,30 +94,49 @@ class Config:
""" """
The configuration file structure. The configuration file structure.
""" """
system: str # all members have default values, so we can easily create
db: str # a default configuration
openai: OpenAIConfig db: str = './db/'
ais: dict[str, AIConfig] = field(default_factory=create_default_ai_configs)
@classmethod @classmethod
def from_dict(cls: Type[ConfigInst], source: dict[str, Any]) -> ConfigInst: def from_dict(cls: Type[ConfigInst], source: dict[str, Any]) -> ConfigInst:
""" """
Create Config from a dict. Create Config from a dict.
""" """
# create the correct AI type instances
ais: dict[str, AIConfig] = {}
for ID, conf in source['ais'].items():
# add the AI ID to the config (for easy internal access)
conf['ID'] = ID
ai_conf = ai_config_instance(conf['name'], conf)
ais[ID] = ai_conf
return cls( return cls(
system=str(source['system']),
db=str(source['db']), db=str(source['db']),
openai=OpenAIConfig.from_dict(source['openai']) ais=ais
) )
@classmethod
def create_default(self, file_path: Path) -> None:
"""
Creates a default Config in the given file.
"""
conf = Config()
conf.to_file(file_path)
@classmethod @classmethod
def from_file(cls: Type[ConfigInst], path: str) -> ConfigInst: def from_file(cls: Type[ConfigInst], path: str) -> ConfigInst:
with open(path, 'r') as f: with open(path, 'r') as f:
source = yaml.load(f, Loader=yaml.FullLoader) source = yaml.load(f, Loader=yaml.FullLoader)
return cls.from_dict(source) return cls.from_dict(source)
def to_file(self, path: str) -> None: def to_file(self, file_path: Path) -> None:
with open(path, 'w') as f: # remove the AI name from the config (for a cleaner format)
yaml.dump(asdict(self), f, sort_keys=False) data = self.as_dict()
for conf in data['ais'].values():
del (conf['ID'])
with open(file_path, 'w') as f:
yaml.dump(data, f, sort_keys=False)
def as_dict(self) -> dict[str, Any]: def as_dict(self) -> dict[str, Any]:
return asdict(self) return asdict(self)