mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-07 03:57:45 +00:00
@@ -0,0 +1,47 @@
|
||||
# TODO: use pydantic for other modules in Qlib
|
||||
# from pydantic_settings import BaseSettings
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# make sure that env variable is loaded while calling Config()
|
||||
load_dotenv(verbose=True, override=True)
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class FincoSettings(BaseSettings):
|
||||
use_azure: bool = True
|
||||
max_retry: int = 10
|
||||
retry_wait_seconds: int = 1
|
||||
continuous_mode: bool = False
|
||||
dump_chat_cache: bool = False
|
||||
use_chat_cache: bool = False
|
||||
dump_embedding_cache: bool = False
|
||||
use_embedding_cache: bool = False
|
||||
prompt_cache_path: str = os.getcwd() + "/prompt_cache.db"
|
||||
session_cache_folder_location: str = os.getcwd() + "/session_cache_folder/"
|
||||
max_past_message_include: int = 10
|
||||
|
||||
log_llm_chat_content: bool = True
|
||||
|
||||
# Chat configs
|
||||
chat_openai_api_key: str = ""
|
||||
chat_azure_api_base: str = ""
|
||||
chat_azure_api_version: str = ""
|
||||
chat_model: str = ""
|
||||
chat_max_tokens: int = 3000
|
||||
chat_temperature: float = 0.5
|
||||
chat_stream: bool = True
|
||||
chat_seed: Union[int, None] = None
|
||||
chat_frequency_penalty: float = 0.0
|
||||
chat_presence_penalty: float = 0.0
|
||||
|
||||
default_system_prompt: str = "You are an AI assistant who helps to answer user's questions about finance."
|
||||
|
||||
# Embedding configs
|
||||
embedding_openai_api_key: str = ""
|
||||
embedding_azure_api_base: str = ""
|
||||
embedding_azure_api_version: str = ""
|
||||
embedding_model: str = ""
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Feedback:
|
||||
pass
|
||||
|
||||
|
||||
class Knowledge:
|
||||
pass
|
||||
|
||||
|
||||
class QueriedKnowledge:
|
||||
pass
|
||||
|
||||
|
||||
class KnowledgeBase(ABC):
|
||||
@abstractmethod
|
||||
def query(
|
||||
self,
|
||||
) -> QueriedKnowledge | None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EvolvableSubjects:
|
||||
"""The target object to be evolved"""
|
||||
|
||||
def clone(self) -> EvolvableSubjects:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
class QlibEvolvableSubjects(EvolvableSubjects): ...
|
||||
|
||||
|
||||
class Evaluator(ABC):
|
||||
"""Both external EvolvableSubjects and internal evovler, it is
|
||||
|
||||
FAQ:
|
||||
- Q: If we have a external whitebox evaluator, do we need a
|
||||
intenral EvolvableSubjects?
|
||||
A: When the external evovler is very complex, maybe a internal LLM-based evovler
|
||||
may provide more understandable feedbacks.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def evaluate(self, evo: EvolvableSubjects, **kwargs: Any) -> Feedback:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SelfEvaluator(Evaluator):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvoStep:
|
||||
"""At a specific step,
|
||||
based on
|
||||
- previous trace
|
||||
- newly RAG kownledge `QueriedKnowledge`
|
||||
|
||||
the EvolvableSubjects is evolved to a new one `EvolvableSubjects`.
|
||||
|
||||
(optional) After evaluation, we get feedback `feedback`.
|
||||
"""
|
||||
|
||||
evolvable_subjects: EvolvableSubjects
|
||||
queried_knowledge: QueriedKnowledge | None = None
|
||||
feedback: Feedback | None = None
|
||||
|
||||
|
||||
class EvolvingStrategy(ABC):
|
||||
@abstractmethod
|
||||
def evolve(
|
||||
self,
|
||||
*evo: EvolvableSubjects,
|
||||
evolving_trace: list[EvoStep] | None = None,
|
||||
queried_knowledge: QueriedKnowledge | None = None,
|
||||
**kwargs: Any,
|
||||
) -> EvolvableSubjects:
|
||||
"""The evolving trace is a list of (evolvable_subjects, feedback) ordered
|
||||
according to the time.
|
||||
|
||||
The reason why the parameter is important for the evolving.
|
||||
- evolving_trace: the historical feedback is important.
|
||||
- queried_knowledge: queried knowledge
|
||||
"""
|
||||
|
||||
|
||||
class RAGStrategy(ABC):
|
||||
"""Retrival Augmentation Generation Strategy"""
|
||||
|
||||
def __init__(self, knowledgebase: KnowledgeBase) -> None:
|
||||
self.knowledgebase = knowledgebase
|
||||
|
||||
@abstractmethod
|
||||
def query(
|
||||
self,
|
||||
evo: EvolvableSubjects,
|
||||
evolving_trace: list[EvoStep],
|
||||
**kwargs: Any,
|
||||
) -> QueriedKnowledge | None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def generate_knowledge(
|
||||
self,
|
||||
evolving_trace: list[EvoStep],
|
||||
*,
|
||||
return_knowledge: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Knowledge | None:
|
||||
"""Generating new knowledge based on the evolving trace.
|
||||
- It is encouraged to query related knowledge before generating new knowledge.
|
||||
|
||||
RAGStrategy should maintain the new knowledge all by itself.
|
||||
"""
|
||||
|
||||
|
||||
class EvoAgent:
|
||||
"""It is responsible for driving the workflow."""
|
||||
|
||||
evolving_trace: list[EvoStep]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
evolving_strategy: EvolvingStrategy,
|
||||
rag: RAGStrategy | None = None,
|
||||
) -> None:
|
||||
self.evolving_trace = []
|
||||
self.evolving_strategy = evolving_strategy
|
||||
self.rag = rag
|
||||
|
||||
def step_evolving(
|
||||
self,
|
||||
evo: EvolvableSubjects,
|
||||
eva: Evaluator | Feedback,
|
||||
*,
|
||||
with_knowledge: bool = False,
|
||||
with_feedback: bool = True,
|
||||
knowledge_self_gen: bool = False,
|
||||
) -> EvolvableSubjects:
|
||||
"""Common evolving mode are supported in this api .
|
||||
- Interactive evolving:
|
||||
- `with_feedback=True` and `eva` is a external Evaluator.
|
||||
|
||||
- Knowledge-driven evolving:
|
||||
- `with_knowledge=True` and related knowledge are
|
||||
queried based on `self.rag`
|
||||
|
||||
- Self-evolving: we have two ways to self-evolve.
|
||||
- 1) self generating knowledge and then evolve
|
||||
- `knowledge_self_gen=True` and `with_knowledge=True`
|
||||
- 2) self evaluate to generate feedback and then evolve
|
||||
- `with_feedback=True` and `eva` is a internal Evaluator.
|
||||
"""
|
||||
# knowledge self-evolving
|
||||
if knowledge_self_gen and self.rag is not None:
|
||||
self.rag.generate_knowledge(self.evolving_trace)
|
||||
|
||||
# RAG
|
||||
queried_knowledge = None
|
||||
if with_knowledge and self.rag is not None:
|
||||
queried_knowledge = self.rag.query(evo, self.evolving_trace)
|
||||
|
||||
# Evolve
|
||||
evo = self.evolving_strategy.evolve(
|
||||
evo=evo,
|
||||
evolving_trace=self.evolving_trace,
|
||||
queried_knowledge=queried_knowledge,
|
||||
)
|
||||
es = EvoStep(evo, queried_knowledge)
|
||||
|
||||
# Evaluate
|
||||
if with_feedback:
|
||||
es.feedback = eva if isinstance(eva, Feedback) else eva.evaluate(evo, queried_knowledge=queried_knowledge)
|
||||
|
||||
# Update trace
|
||||
self.evolving_trace.append(es)
|
||||
return evo
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Generator, Sequence
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from loguru import Logger
|
||||
|
||||
|
||||
class LogColors:
|
||||
"""
|
||||
ANSI color codes for use in console output.
|
||||
"""
|
||||
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
BLUE = "\033[94m"
|
||||
MAGENTA = "\033[95m"
|
||||
CYAN = "\033[96m"
|
||||
WHITE = "\033[97m"
|
||||
GRAY = "\033[90m"
|
||||
BLACK = "\033[30m"
|
||||
|
||||
BOLD = "\033[1m"
|
||||
ITALIC = "\033[3m"
|
||||
|
||||
END = "\033[0m"
|
||||
|
||||
@classmethod
|
||||
def get_all_colors(cls: type[LogColors]) -> list:
|
||||
names = dir(cls)
|
||||
names = [name for name in names if not name.startswith("__") and not callable(getattr(cls, name))]
|
||||
return [getattr(cls, name) for name in names]
|
||||
|
||||
def render(self, text: str, color: str = "", style: str = "") -> str:
|
||||
"""
|
||||
render text by input color and style.
|
||||
It's not recommend that input text is already rendered.
|
||||
"""
|
||||
# This method is called too frequently, which is not good.
|
||||
colors = self.get_all_colors()
|
||||
# Perhaps color and font should be distinguished here.
|
||||
if color and color in colors:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# raise ValueError(f"color should be in: {colors} but now is: {color}")
|
||||
# Description of the problem:
|
||||
# TRY003 Avoid specifying long messages outside the exception class
|
||||
# EM102 Exception must not use an f-string literal, assign to variable first
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/raise-vanilla-args/
|
||||
# https://docs.astral.sh/ruff/rules/f-string-in-exception/
|
||||
error_message = f"color should be in: {colors} but now is: {color}"
|
||||
raise ValueError(error_message)
|
||||
if style and style in colors:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# raise ValueError(f"style should be in: {colors} but now is: {style}")
|
||||
# Description of the problem:
|
||||
# TRY003 Avoid specifying long messages outside the exception class
|
||||
# EM102 Exception must not use an f-string literal, assign to variable first
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/raise-vanilla-args/
|
||||
# https://docs.astral.sh/ruff/rules/f-string-in-exception/
|
||||
error_message = f"style should be in: {colors} but now is: {style}"
|
||||
raise ValueError(error_message)
|
||||
|
||||
text = f"{color}{text}{self.END}"
|
||||
|
||||
return f"{style}{text}{self.END}"
|
||||
|
||||
|
||||
class FinCoLog:
|
||||
# logger.add(loguru_handler, level="INFO") # you can add use storage as a loguru handler
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.logger: Logger = logger
|
||||
|
||||
def info(self, *args: Sequence, plain: bool = False, title: str = "Info") -> None:
|
||||
if plain:
|
||||
return self.plain_info(*args)
|
||||
for arg in args:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# self.logger.info(f"{LogColors.WHITE}{arg}{LogColors.END}")
|
||||
# Description of the problem:
|
||||
# G004 Logging statement uses f-string
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/logging-f-string/
|
||||
info = f"{LogColors.WHITE}{arg}{LogColors.END}"
|
||||
self.logger.info(info)
|
||||
return None
|
||||
|
||||
def __getstate__(self) -> dict:
|
||||
return {}
|
||||
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code: def __setstate__(self, _: str) -> None:
|
||||
# Description of the problem:
|
||||
# PLE0302 The special method `__setstate__` expects 2 parameters, 1 was given
|
||||
# References: https://docs.astral.sh/ruff/rules/unexpected-special-method-signature/
|
||||
def __setstate__(self, _: str) -> None:
|
||||
self.logger = logger
|
||||
|
||||
def plain_info(self, *args: Sequence) -> None:
|
||||
for arg in args:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# self.logger.info(
|
||||
# f"""
|
||||
# {LogColors.YELLOW}{LogColors.BOLD}
|
||||
# Info:{LogColors.END}{LogColors.WHITE}{arg}{LogColors.END}
|
||||
# """,
|
||||
# )
|
||||
# Description of the problem:
|
||||
# G004 Logging statement uses f-string
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/logging-f-string/
|
||||
info = f"""
|
||||
{LogColors.YELLOW}{LogColors.BOLD}
|
||||
Info:{LogColors.END}{LogColors.WHITE}{arg}{LogColors.END}
|
||||
"""
|
||||
self.logger.info(info)
|
||||
|
||||
def warning(self, *args: Sequence) -> None:
|
||||
for arg in args:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# self.logger.warning(
|
||||
# f"{LogColors.BLUE}{LogColors.BOLD}Warning:{LogColors.END}{arg}",
|
||||
# )
|
||||
# Description of the problem:
|
||||
# G004 Logging statement uses f-string
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/logging-f-string/
|
||||
info = f"{LogColors.BLUE}{LogColors.BOLD}Warning:{LogColors.END}{arg}"
|
||||
self.logger.warning(info)
|
||||
|
||||
def error(self, *args: Sequence) -> None:
|
||||
for arg in args:
|
||||
# Changes to accommodate ruff checks.
|
||||
# Original code:
|
||||
# self.logger.error(
|
||||
# f"{LogColors.RED}{LogColors.BOLD}Error:{LogColors.END}{arg}",
|
||||
# )
|
||||
# Description of the problem:
|
||||
# G004 Logging statement uses f-string
|
||||
# References:
|
||||
# https://docs.astral.sh/ruff/rules/logging-f-string/
|
||||
info = f"{LogColors.RED}{LogColors.BOLD}Error:{LogColors.END}{arg}"
|
||||
self.logger.error(info)
|
||||
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
import multiprocessing as mp
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class FincoException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
_instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(SingletonMeta, cls).__call__(*args, **kwargs)
|
||||
return cls._instance
|
||||
|
||||
|
||||
class SingletonBaseClass(metaclass=SingletonMeta):
|
||||
"""
|
||||
Because we try to support defining Singleton with `class A(SingletonBaseClass)` instead of `A(metaclass=SingletonMeta)`
|
||||
This class becomes necessary
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Add move this class to Qlib's general utils.
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
try:
|
||||
return json.loads(response)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
|
||||
raise Exception(f"Failed to parse response: {response}, please report it or help us to fix it.")
|
||||
|
||||
|
||||
def similarity(text1, text2):
|
||||
text1 = text1 if isinstance(text1, str) else ""
|
||||
text2 = text2 if isinstance(text2, str) else ""
|
||||
|
||||
# Maybe we can use other similarity algorithm such as tfidf
|
||||
return fuzz.ratio(text1, text2)
|
||||
|
||||
|
||||
def random_string(length=10):
|
||||
letters = string.ascii_letters + string.digits
|
||||
return "".join(random.choice(letters) for i in range(length))
|
||||
|
||||
|
||||
def remove_uncommon_keys(new_dict, org_dict):
|
||||
keys_to_remove = []
|
||||
|
||||
for key in new_dict:
|
||||
if key not in org_dict:
|
||||
keys_to_remove.append(key)
|
||||
elif isinstance(new_dict[key], dict) and isinstance(org_dict[key], dict):
|
||||
remove_uncommon_keys(new_dict[key], org_dict[key])
|
||||
elif isinstance(new_dict[key], dict) and isinstance(org_dict[key], str):
|
||||
new_dict[key] = org_dict[key]
|
||||
|
||||
for key in keys_to_remove:
|
||||
del new_dict[key]
|
||||
|
||||
|
||||
def crawl_the_folder(folder_path: Path):
|
||||
yaml_files = []
|
||||
for root, _, files in os.walk(folder_path.as_posix()):
|
||||
for file in files:
|
||||
if file.endswith(".yaml") or file.endswith(".yml"):
|
||||
yaml_file_path = Path(os.path.join(root, file)).relative_to(folder_path)
|
||||
yaml_files.append(yaml_file_path.as_posix())
|
||||
return sorted(yaml_files)
|
||||
|
||||
|
||||
def compare_yaml(file1, file2):
|
||||
with open(file1, "r") as stream:
|
||||
data1 = yaml.safe_load(stream)
|
||||
with open(file2, "r") as stream:
|
||||
data2 = yaml.safe_load(stream)
|
||||
return data1 == data2
|
||||
|
||||
|
||||
def remove_keys(valid_keys, ori_dict):
|
||||
for key in list(ori_dict.keys()):
|
||||
if key not in valid_keys:
|
||||
ori_dict.pop(key)
|
||||
return ori_dict
|
||||
|
||||
|
||||
class YamlConfigCache(SingletonBaseClass):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.path_to_config = dict()
|
||||
|
||||
def load(self, path):
|
||||
with open(path, "r") as stream:
|
||||
data = yaml.safe_load(stream)
|
||||
self.path_to_config[path] = data
|
||||
|
||||
def __getitem__(self, path):
|
||||
if path not in self.path_to_config:
|
||||
self.load(path)
|
||||
return self.path_to_config[path]
|
||||
|
||||
|
||||
def import_class(class_path: str) -> Any:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
class_path : str
|
||||
class path like"scripts.factor_implementation.baselines.naive.one_shot.OneshotFactorGen"
|
||||
|
||||
Returns
|
||||
-------
|
||||
class of `class_path`
|
||||
"""
|
||||
module_path, class_name = class_path.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
|
||||
|
||||
def multiprocessing_wrapper(func_calls: list[tuple[Callable, tuple]], n: int) -> list:
|
||||
"""It will use multiprocessing to call the functions in func_calls with the given parameters.
|
||||
The results equals to `return [f(*args) for f, args in func_calls]`
|
||||
It will not call multiprocessing if `n=1`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func_calls : List[Tuple[Callable, Tuple]]
|
||||
the list of functions and their parameters
|
||||
n : int
|
||||
the number of subprocesses
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
|
||||
"""
|
||||
if n == 1:
|
||||
return [f(*args) for f, args in func_calls]
|
||||
with mp.Pool(processes=n) as pool:
|
||||
results = [pool.apply_async(f, args) for f, args in func_calls]
|
||||
return [result.get() for result in results]
|
||||
|
||||
|
||||
# You can test the above function
|
||||
# def f(x):
|
||||
# return x**2
|
||||
#
|
||||
# if __name__ == "__main__":
|
||||
# print(multiprocessing_wrapper([(f, (i,)) for i in range(10)], 4))
|
||||
Reference in New Issue
Block a user