Compare commits
7 Commits
5c46bbe865
...
d8aeb6cb0c
| Author | SHA1 | Date | |
|---|---|---|---|
| d8aeb6cb0c | |||
| 6d085a6c80 | |||
| 6b4ce8448f | |||
| ba16e325d1 | |||
| 42723d40ed | |||
| 4538624247 | |||
| f964c5471e |
@ -3,18 +3,20 @@ Creates different AI instances, based on the given configuration.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from typing import cast
|
||||
from typing import cast, Optional
|
||||
from .configuration import Config, AIConfig, OpenAIConfig
|
||||
from .ai import AI, AIError
|
||||
from .ais.openai import OpenAI
|
||||
|
||||
|
||||
def create_ai(args: argparse.Namespace, config: Config) -> AI: # noqa: 11
|
||||
def create_ai(args: argparse.Namespace, config: Config, # noqa: 11
|
||||
def_ai: Optional[str] = None,
|
||||
def_model: Optional[str] = None) -> AI:
|
||||
"""
|
||||
Creates an AI subclass instance from the given arguments
|
||||
and configuration file. If AI has not been set in the
|
||||
arguments, it searches for the ID 'default'. If that
|
||||
is not found, it uses the first AI in the list.
|
||||
Creates an AI subclass instance from the given arguments and configuration file.
|
||||
If AI has not been set in the arguments, it searches for the ID 'default'. If
|
||||
that is not found, it uses the first AI in the list. It's also possible to
|
||||
specify a default AI and model using 'def_ai' and 'def_model'.
|
||||
"""
|
||||
ai_conf: AIConfig
|
||||
if hasattr(args, 'AI') and args.AI:
|
||||
@ -22,6 +24,8 @@ def create_ai(args: argparse.Namespace, config: Config) -> AI: # noqa: 11
|
||||
ai_conf = config.ais[args.AI]
|
||||
except KeyError:
|
||||
raise AIError(f"AI ID '{args.AI}' does not exist in this configuration")
|
||||
elif def_ai:
|
||||
ai_conf = config.ais[def_ai]
|
||||
elif 'default' in config.ais:
|
||||
ai_conf = config.ais['default']
|
||||
else:
|
||||
@ -34,6 +38,8 @@ def create_ai(args: argparse.Namespace, config: Config) -> AI: # noqa: 11
|
||||
ai = OpenAI(cast(OpenAIConfig, ai_conf))
|
||||
if hasattr(args, 'model') and args.model:
|
||||
ai.config.model = args.model
|
||||
elif def_model:
|
||||
ai.config.model = def_model
|
||||
if hasattr(args, 'max_tokens') and args.max_tokens:
|
||||
ai.config.max_tokens = args.max_tokens
|
||||
if hasattr(args, 'temperature') and args.temperature:
|
||||
|
||||
@ -2,6 +2,7 @@ import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from itertools import zip_longest
|
||||
from copy import deepcopy
|
||||
from ..configuration import Config
|
||||
from ..chat import ChatDB
|
||||
from ..message import Message, MessageFilter, MessageError, Question, source_code
|
||||
@ -105,13 +106,79 @@ def make_request(ai: AI, chat: ChatDB, message: Message, args: argparse.Namespac
|
||||
print(response.tokens)
|
||||
|
||||
|
||||
def make_msg_args(msg: Message, args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
Takes an existing message and CLI arguments, and returns modified args based
|
||||
on the members of the given message. Used e.g. when repeating messages, where
|
||||
it's necessary to determine the correct AI, module and output tags to use
|
||||
(either from the existing message or the given args).
|
||||
"""
|
||||
msg_args = args
|
||||
# if AI, model or output tags have not been specified,
|
||||
# use those from the original message
|
||||
if (args.AI is None
|
||||
or args.model is None # noqa: W503
|
||||
or args.output_tags is None # noqa: W503
|
||||
or len(args.output_tags) == 0): # noqa: W503
|
||||
msg_args = deepcopy(args)
|
||||
if args.AI is None and msg.ai is not None:
|
||||
msg_args.AI = msg.ai
|
||||
if args.model is None and msg.model is not None:
|
||||
msg_args.model = msg.model
|
||||
if args.output_tags is None and msg.tags is not None:
|
||||
msg_args.output_tags = msg.tags
|
||||
return msg_args
|
||||
|
||||
|
||||
def repeat_messages(messages: list[Message], chat: ChatDB, args: argparse.Namespace, config: Config) -> None:
|
||||
"""
|
||||
Repeat the given messages using the given arguments.
|
||||
"""
|
||||
ai: AI
|
||||
for msg in messages:
|
||||
ai = create_ai(make_msg_args(msg, args), config)
|
||||
print(f"--------- Repeating message '{msg.msg_id()}': ---------")
|
||||
# overwrite the latest message if requested or empty
|
||||
# -> but not if it's in the DB!
|
||||
if ((msg.answer is None or args.overwrite is True)
|
||||
and (not chat.msg_in_db(msg))): # noqa: W503
|
||||
msg.clear_answer()
|
||||
make_request(ai, chat, msg, args)
|
||||
# otherwise create a new one
|
||||
else:
|
||||
args.ask = [msg.question]
|
||||
message = create_message(chat, args)
|
||||
make_request(ai, chat, message, args)
|
||||
|
||||
|
||||
def modify_tag_args(args: argparse.Namespace) -> None:
|
||||
"""
|
||||
Changes the semantics of the INPUT tags for this command:
|
||||
* not tags specified on the CLI -> no tags are selected
|
||||
* empty tags specified on the CLI -> all tags are selected
|
||||
"""
|
||||
if args.or_tags is None:
|
||||
args.or_tags = set()
|
||||
elif len(args.or_tags) == 0:
|
||||
args.or_tags = None
|
||||
if args.and_tags is None:
|
||||
args.and_tags = set()
|
||||
elif len(args.and_tags) == 0:
|
||||
args.and_tags = None
|
||||
if args.exclude_tags is None:
|
||||
args.exclude_tags = set()
|
||||
elif len(args.exclude_tags) == 0:
|
||||
args.exclude_tags = None
|
||||
|
||||
|
||||
def question_cmd(args: argparse.Namespace, config: Config) -> None:
|
||||
"""
|
||||
Handler for the 'question' command.
|
||||
"""
|
||||
mfilter = MessageFilter(tags_or=args.or_tags if args.or_tags is not None else set(),
|
||||
tags_and=args.and_tags if args.and_tags is not None else set(),
|
||||
tags_not=args.exclude_tags if args.exclude_tags is not None else set())
|
||||
modify_tag_args(args)
|
||||
mfilter = MessageFilter(tags_or=args.or_tags,
|
||||
tags_and=args.and_tags,
|
||||
tags_not=args.exclude_tags)
|
||||
chat = ChatDB.from_dir(cache_path=Path(config.cache),
|
||||
db_path=Path(config.db),
|
||||
mfilter=mfilter)
|
||||
@ -121,30 +188,24 @@ def question_cmd(args: argparse.Namespace, config: Config) -> None:
|
||||
if args.create:
|
||||
return
|
||||
|
||||
# create the correct AI instance
|
||||
ai: AI = create_ai(args, config)
|
||||
|
||||
# === ASK ===
|
||||
if args.ask:
|
||||
ai: AI = create_ai(args, config)
|
||||
make_request(ai, chat, message, args)
|
||||
# === REPEAT ===
|
||||
elif args.repeat is not None:
|
||||
lmessage = chat.msg_latest(loc='cache')
|
||||
if lmessage is None:
|
||||
print("No message found to repeat!")
|
||||
sys.exit(1)
|
||||
repeat_msgs: list[Message] = []
|
||||
# repeat latest message
|
||||
if len(args.repeat) == 0:
|
||||
lmessage = chat.msg_latest(loc='cache')
|
||||
if lmessage is None:
|
||||
print("No message found to repeat!")
|
||||
sys.exit(1)
|
||||
repeat_msgs.append(lmessage)
|
||||
# repeat given message(s)
|
||||
else:
|
||||
print(f"Repeating message '{lmessage.msg_id()}':")
|
||||
# overwrite the latest message if requested or empty
|
||||
if lmessage.answer is None or args.overwrite is True:
|
||||
lmessage.clear_answer()
|
||||
make_request(ai, chat, lmessage, args)
|
||||
# otherwise create a new one
|
||||
else:
|
||||
args.ask = [lmessage.question]
|
||||
message = create_message(chat, args)
|
||||
make_request(ai, chat, message, args)
|
||||
|
||||
repeat_msgs = chat.msg_find(args.repeat, loc='disk')
|
||||
repeat_messages(repeat_msgs, chat, args, config)
|
||||
# === PROCESS ===
|
||||
elif args.process is not None:
|
||||
# TODO: process either all questions without an
|
||||
|
||||
@ -34,13 +34,13 @@ def create_parser() -> argparse.ArgumentParser:
|
||||
|
||||
# a parent parser for all commands that support tag selection
|
||||
tag_parser = argparse.ArgumentParser(add_help=False)
|
||||
tag_arg = tag_parser.add_argument('-t', '--or-tags', nargs='+',
|
||||
tag_arg = tag_parser.add_argument('-t', '--or-tags', nargs='*',
|
||||
help='List of tags (one must match)', metavar='OTAGS')
|
||||
tag_arg.completer = tags_completer # type: ignore
|
||||
atag_arg = tag_parser.add_argument('-k', '--and-tags', nargs='+',
|
||||
atag_arg = tag_parser.add_argument('-k', '--and-tags', nargs='*',
|
||||
help='List of tags (all must match)', metavar='ATAGS')
|
||||
atag_arg.completer = tags_completer # type: ignore
|
||||
etag_arg = tag_parser.add_argument('-x', '--exclude-tags', nargs='+',
|
||||
etag_arg = tag_parser.add_argument('-x', '--exclude-tags', nargs='*',
|
||||
help='List of tags to exclude', metavar='XTAGS')
|
||||
etag_arg.completer = tags_completer # type: ignore
|
||||
otag_arg = tag_parser.add_argument('-o', '--output-tags', nargs='+',
|
||||
|
||||
@ -282,6 +282,9 @@ class TestQuestionCmd(TestQuestionCmdBase):
|
||||
# exclude '.next'
|
||||
return sorted([f for f in Path(tmp_dir.name).glob('*.[ty]*')])
|
||||
|
||||
|
||||
class TestQuestionCmdAsk(TestQuestionCmd):
|
||||
|
||||
@mock.patch('chatmastermind.commands.question.create_ai')
|
||||
def test_ask_single_answer(self, mock_create_ai: MagicMock) -> None:
|
||||
"""
|
||||
@ -370,6 +373,9 @@ class TestQuestionCmd(TestQuestionCmdBase):
|
||||
self.assertEqual(len(self.message_list(self.cache_dir)), 1)
|
||||
self.assert_messages_equal(cached_msg, [expected_question])
|
||||
|
||||
|
||||
class TestQuestionCmdRepeat(TestQuestionCmd):
|
||||
|
||||
@mock.patch('chatmastermind.commands.question.create_ai')
|
||||
def test_repeat_single_question(self, mock_create_ai: MagicMock) -> None:
|
||||
"""
|
||||
@ -511,3 +517,44 @@ class TestQuestionCmd(TestQuestionCmdBase):
|
||||
cached_msg = chat.msg_gather(loc='cache')
|
||||
self.assertEqual(len(self.message_list(self.cache_dir)), 2)
|
||||
self.assert_messages_equal(cached_msg, expected_responses)
|
||||
|
||||
@mock.patch('chatmastermind.commands.question.create_ai')
|
||||
def test_repeat_multiple_questions(self, mock_create_ai: MagicMock) -> None:
|
||||
"""
|
||||
Repeat multiple questions.
|
||||
"""
|
||||
# 1. create some questions / messages
|
||||
# cached message without an answer
|
||||
message1 = Message(Question('Question 1'),
|
||||
ai='foo',
|
||||
model='bla',
|
||||
file_path=Path(self.cache_dir.name) / '0001.txt')
|
||||
# cached message with an answer
|
||||
message2 = Message(Question('Question 2'),
|
||||
Answer('Answer 2'),
|
||||
ai='openai',
|
||||
model='gpt-3.5-turbo',
|
||||
file_path=Path(self.cache_dir.name) / '0002.txt')
|
||||
# DB message without an answer
|
||||
message3 = Message(Question('Question 3'),
|
||||
ai='openai',
|
||||
model='gpt-3.5-turbo',
|
||||
file_path=Path(self.db_dir.name) / '0003.txt')
|
||||
message1.to_file()
|
||||
message2.to_file()
|
||||
message3.to_file()
|
||||
# chat = ChatDB.from_dir(Path(self.cache_dir.name),
|
||||
# Path(self.db_dir.name))
|
||||
|
||||
# 2. repeat all three questions (without overwriting)
|
||||
self.args.ask = None
|
||||
self.args.repeat = ['0001', '0002', '0003']
|
||||
self.args.overwrite = False
|
||||
question_cmd(self.args, self.config)
|
||||
# two new files should be in the cache directory
|
||||
# * the repeated cached message with answer
|
||||
# * the repeated DB message
|
||||
# -> the cached message wihtout answer should be overwritten
|
||||
self.assertEqual(len(self.message_list(self.cache_dir)), 4)
|
||||
self.assertEqual(len(self.message_list(self.db_dir)), 1)
|
||||
# FIXME: also compare actual content!
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user