1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-19 02:14:33 +08:00

Init workspace and CMDTask (#1537)

* Update setup.py and config

* WIP

* init_workspace and CMDTask

* Delete test_sumarize.py
This commit is contained in:
you-n-g
2023-06-01 23:32:35 +08:00
committed by GitHub
parent 3b56b8e6c0
commit 73d51f05b4
9 changed files with 209 additions and 90 deletions

View File

@@ -1,10 +1,16 @@
# TODO: use pydantic for other modules in Qlib
from pydantic import (BaseSettings)
from pydantic import BaseSettings
from qlib.finco.utils import Singleton
import os
class Config(Singleton):
"""
This config is for fast demo purpose.
Please use BaseSettings insetead in the future
"""
def __init__(self):
self.use_azure = os.getenv("USE_AZURE") == "True"
self.temperature = 0.5 if os.getenv("TEMPERATURE") is None else float(os.getenv("TEMPERATURE"))
@@ -18,5 +24,8 @@ class Config(Singleton):
self.max_retry = int(os.getenv("MAX_RETRY")) if os.getenv("MAX_RETRY") is not None else None
self.continous_mode = os.getenv("CONTINOUS_MODE") == "True" if os.getenv("CONTINOUS_MODE") is not None else False
self.continous_mode = (
os.getenv("CONTINOUS_MODE") == "True" if os.getenv("CONTINOUS_MODE") is not None else False
)
self.debug_mode = os.getenv("DEBUG_MODE") == "True" if os.getenv("DEBUG_MODE") is not None else False
self.workspace = os.getenv("WORKSPACE") if os.getenv("WORKSPACE") is not None else "./finco_workspace"

View File

@@ -23,7 +23,9 @@ class APIBackend(Singleton):
self.debug_mode = True
cwd = os.getcwd()
self.cache_file_location = os.path.join(cwd, "prompt_cache.json")
self.cache = json.load(open(self.cache_file_location, "r")) if os.path.exists(self.cache_file_location) else {}
self.cache = (
json.load(open(self.cache_file_location, "r")) if os.path.exists(self.cache_file_location) else {}
)
def build_messages_and_create_chat_completion(self, user_prompt, system_prompt=None):
"""build the messages to avoid implementing several redundant lines of code"""

View File

@@ -6,11 +6,15 @@ from jinja2 import Template
import abc
import re
import logging
import subprocess
import platform
from qlib.log import get_module_logger
from qlib.finco.llm import APIBackend
from qlib.finco.tpl import get_tpl_path
class Task():
class Task:
"""
The user's intention, which was initially represented by a prompt, is achieved through a sequence of tasks.
This class doesn't have to be abstract, but it is abstract in the sense that it is not supposed to be instantiated directly because it doesn't have any implementation.
@@ -65,10 +69,12 @@ class Task():
raise NotImplementedError("The interact method is not implemented, but workflow not in continous mode")
class WorkflowTask(Task):
"""This task is supposed to be the first task of the workflow"""
def __init__(self,) -> None:
def __init__(
self,
) -> None:
super().__init__()
self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT = """
Your task is to determine the workflow in Qlib (supervised learning or reinforcement learning) ensuring the workflow can meet the user's requirements.
@@ -94,14 +100,18 @@ workflow: reinforcement learning
"Response only with the output in the exact format specified in the system prompt, with no explanation or conversation.\n"
)
def execute(self,) -> List[Task]:
def execute(
self,
) -> List[Task]:
"""make the choice which main workflow (RL, SL) will be used"""
user_prompt = self._context_manager.get_context("user_prompt")
prompt_workflow_selection = Template(
self.__DEFAULT_WORKFLOW_USER_PROMPT
).render(user_prompt=user_prompt)
response = APIBackend().build_messages_and_create_chat_completion(prompt_workflow_selection, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT)
self.save_chat_history_to_context_manager(prompt_workflow_selection, response, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT)
prompt_workflow_selection = Template(self.__DEFAULT_WORKFLOW_USER_PROMPT).render(user_prompt=user_prompt)
response = APIBackend().build_messages_and_create_chat_completion(
prompt_workflow_selection, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT
)
self.save_chat_history_to_context_manager(
prompt_workflow_selection, response, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT
)
workflow = response.split(":")[1].strip().lower()
self.executed = True
self._context_manager.set_context("workflow", workflow)
@@ -137,8 +147,11 @@ workflow: reinforcement learning
class PlanTask(Task):
pass
class SLPlanTask(PlanTask):
def __init__(self,) -> None:
def __init__(
self,
) -> None:
super().__init__()
self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT = """
Your task is to determine the 5 crucial components in Qlib (Dataset, Model, Record, Strategy, Backtest) ensuring the workflow can meet the user's requirements.
@@ -174,13 +187,15 @@ components:
user_prompt = self._context_manager.get_context("user_prompt")
assert user_prompt is not None, "The user prompt is not provided"
prompt_plan_all = Template(
self.__DEFAULT_WORKFLOW_USER_PROMPT
).render(user_prompt=user_prompt)
response = APIBackend().build_messages_and_create_chat_completion(prompt_plan_all, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT)
prompt_plan_all = Template(self.__DEFAULT_WORKFLOW_USER_PROMPT).render(user_prompt=user_prompt)
response = APIBackend().build_messages_and_create_chat_completion(
prompt_plan_all, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT
)
self.save_chat_history_to_context_manager(prompt_plan_all, response, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT)
if "components" not in response:
self.logger.warning("The response is not in the correct format, which probably means the answer is not correct")
self.logger.warning(
"The response is not in the correct format, which probably means the answer is not correct"
)
regex_dict = {
"Dataset": re.compile("Dataset: \((.*?)\) (.*?)\n"),
@@ -190,6 +205,11 @@ components:
"Backtest": re.compile("Backtest: \((.*?)\) (.*?)$"),
}
new_task = []
# 1) create a workspace
# TODO: we have to make choice between `sl` and `sl-cfg`
new_task.append(CMDTask(cmd_intention=f"Copy folder from {get_tpl_path() / 'sl'} to {self._context_manager.get_context('workspace')}"))
# 2) CURD on the workspace
for name, regex in regex_dict.items():
res = re.search(regex, response)
if not res:
@@ -204,8 +224,11 @@ components:
new_task.extend([ConfigActionTask(name), ImplementActionTask(name)])
return new_task
class RLPlanTask(PlanTask):
def __init__(self,) -> None:
def __init__(
self,
) -> None:
super().__init__()
self.logger.error("The RL task is not implemented yet")
exit()
@@ -221,6 +244,45 @@ class RLPlanTask(PlanTask):
class ActionTask(Task):
pass
class CMDTask(ActionTask):
"""
This CMD task is responsible for ensuring compatibility across different operating systems.
"""
__DEFAULT_WORKFLOW_SYSTEM_PROMPT = """
You are an expert system administrator.
Your task is to convert the user's intention into a specific runnable command for a particular system.
Example input:
- User intention: Copy the folder from a/b/c to d/e/f
- User OS: Linux
Example output:
cp -r a/b/c d/e/f
"""
__DEFAULT_WORKFLOW_USER_PROMPT = """
Example input:
- User intention: "{{cmd_intention}}"
- User OS: "{{user_os}}"
Example output:
"""
def __init__(self, cmd_intention: str, cwd=None):
self.cwd = cwd
self.cmd_intention = cmd_intention
self._output = None
def execute(self):
prompt = Template(self.__DEFAULT_WORKFLOW_USER_PROMPT).render(cmd_intention=self.cmd_intention,
user_os=platform.system())
response = APIBackend().build_messages_and_create_chat_completion(prompt, self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT)
self._output = subprocess.check_output(response, shell=True, cwd=self.cwd)
return []
def summarize(self):
if self._output is not None:
# TODO: it will be overrides by later commands
self._context_manager.set_context(self.__class__.__name__, self._output.decode("utf8"))
class ConfigActionTask(ActionTask):
def __init__(self, component) -> None:
super().__init__()
@@ -268,8 +330,7 @@ Reason: I choose the hyperparameters above because they are the default hyperpar
Improve suggestion: You can try to tune the num_leaves in range [100, 300], max_depth in [5, 10], learning_rate in [0.01, 1] and other hyperparameters in the config. Since you're trying to get a long tern return, if you have enough computation resource, you can try to use a larger num_leaves and max_depth and a smaller learning_rate.
"""
self.__CONFIG_ACTION_SYSTEM_PROMPT_TEMPLATE = (
"""
self.__CONFIG_ACTION_SYSTEM_PROMPT_TEMPLATE = """
user requirement: {{user_requirement}}
user plan:
- Dataset: ({{dataset_decision}}) {{dataset_plan}}
@@ -279,7 +340,6 @@ user plan:
- Backtest: ({{backtest_decision}}) {{backtest_plan}}
target component: {{target_component}}
"""
)
def execute(self):
user_prompt = self._context_manager.get_context("user_prompt")
@@ -303,12 +363,16 @@ target component: {{target_component}}
strategy_plan=prompt_element_dict["Strategy_plan"],
backtest_decision=prompt_element_dict["Backtest_decision"],
backtest_plan=prompt_element_dict["Backtest_plan"],
target_component=self.target_componet
target_component=self.target_componet,
)
response = APIBackend().build_messages_and_create_chat_completion(
config_prompt, self.__DEFAULT_CONFIG_ACTION_SYSTEM_PROMPT
)
response = APIBackend().build_messages_and_create_chat_completion(config_prompt, self.__DEFAULT_CONFIG_ACTION_SYSTEM_PROMPT)
self.save_chat_history_to_context_manager(config_prompt, response, self.__DEFAULT_CONFIG_ACTION_SYSTEM_PROMPT)
res = re.search(r"Config:(.*)Reason:(.*)Improve suggestion:(.*)", response, re.S)
assert res is not None and len(res.groups()) == 3, "The response of config action task is not in the correct format"
assert (
res is not None and len(res.groups()) == 3
), "The response of config action task is not in the correct format"
config = re.search(r"```yaml(.*)```", res.group(1), re.S)
assert config is not None, "The config part of config action task response is not in the correct format"
@@ -396,7 +460,7 @@ dataset:
csv_path: path/to/your/csv/data
```
"""
self.__DEFAULT_IMPLEMENT_ACTION_USER_PROMPT = ("""
self.__DEFAULT_IMPLEMENT_ACTION_USER_PROMPT = """
user requirement: {{user_requirement}}
user plan:
- Dataset: ({{dataset_decision}}) {{dataset_plan}}
@@ -409,7 +473,7 @@ User config:
{{user_config}}
```
target component: {{target_component}}
""")
"""
def execute(self):
"""
@@ -440,20 +504,28 @@ target component: {{target_component}}
backtest_decision=prompt_element_dict["Backtest_decision"],
backtest_plan=prompt_element_dict["Backtest_plan"],
target_component=self.target_component,
user_config=config
user_config=config,
)
response = APIBackend().build_messages_and_create_chat_completion(
implement_prompt, self.__DEFAULT_IMPLEMENT_ACTION_SYSTEM_PROMPT
)
self.save_chat_history_to_context_manager(
implement_prompt, response, self.__DEFAULT_IMPLEMENT_ACTION_SYSTEM_PROMPT
)
response = APIBackend().build_messages_and_create_chat_completion(implement_prompt, self.__DEFAULT_IMPLEMENT_ACTION_SYSTEM_PROMPT)
self.save_chat_history_to_context_manager(implement_prompt, response, self.__DEFAULT_IMPLEMENT_ACTION_SYSTEM_PROMPT)
res = re.search(r"Code:(.*)Explanation:(.*)Modified config:(.*)", response, re.S)
assert res is not None and len(res.groups()) == 3, f"The response of implement action task of component {self.target_component} is not in the correct format"
assert (
res is not None and len(res.groups()) == 3
), f"The response of implement action task of component {self.target_component} is not in the correct format"
code = re.search(r"```python(.*)```", res.group(1), re.S)
assert code is not None, "The code part of implementation action task response is not in the correct format"
code = code.group(1)
explanation = res.group(2)
modified_config = re.search(r"```yaml(.*)```", res.group(3), re.S)
assert modified_config is not None, "The modified config part of implementation action task response is not in the correct format"
assert (
modified_config is not None
), "The modified config part of implementation action task response is not in the correct format"
modified_config = modified_config.group(1)
self._context_manager.set_context(f"{self.target_component}_code", code)
@@ -462,8 +534,9 @@ target component: {{target_component}}
return []
class SummarizeTask(Task):
__DEFAULT_OUTPUT_PATH = "./"
__DEFAULT_WORKSPACE = "./"
__DEFAULT_WORKFLOW_SYSTEM_PROMPT = """
You are an expert in quant domain.
@@ -510,7 +583,7 @@ class SummarizeTask(Task):
# TODO: 2048 is close to exceed GPT token limit
__MAX_LENGTH_OF_FILE = 2048
__DEFAULT_REPORT_NAME = 'finCoReport.md'
__DEFAULT_REPORT_NAME = "finCoReport.md"
def __init__(self):
super().__init__()
@@ -519,22 +592,24 @@ class SummarizeTask(Task):
user_prompt = self._context_manager.get_context("user_prompt")
user_prompt = user_prompt if user_prompt is not None else self.__DEFAULT_USER_PROMPT
system_prompt = self.__DEFAULT_WORKFLOW_SYSTEM_PROMPT
output_path = self._context_manager.get_context("output_path")
output_path = output_path if output_path is not None else self.__DEFAULT_OUTPUT_PATH
file_info = self.get_info_from_file(output_path)
workspace = self._context_manager.get_context("workspace")
workspace = workspace if workspace is not None else self.__DEFAULT_WORKSPACE
file_info = self.get_info_from_file(workspace)
context_info = self.get_info_from_context()
information = context_info + file_info
prompt_workflow_selection = Template(self.__DEFAULT_WORKFLOW_USER_PROMPT).render(information=information,
user_prompt=user_prompt)
prompt_workflow_selection = Template(self.__DEFAULT_WORKFLOW_USER_PROMPT).render(
information=information, user_prompt=user_prompt
)
response = APIBackend().build_messages_and_create_chat_completion(user_prompt=prompt_workflow_selection,
system_prompt=system_prompt)
response = APIBackend().build_messages_and_create_chat_completion(
user_prompt=prompt_workflow_selection, system_prompt=system_prompt
)
self.save_markdown(content=response)
return []
def summarize(self) -> str:
return ''
return ""
def interact(self) -> Any:
return
@@ -552,22 +627,29 @@ class SummarizeTask(Task):
result = []
for file in file_list:
postfix = file.split('.')[-1]
if postfix in ['py', 'log', 'yaml']:
postfix = file.split(".")[-1]
if postfix in ["py", "log", "yaml"]:
with open(file) as f:
content = f.read()
self.logger.info(f"file to summarize: {file}")
# in case of too large file
# TODO: Perhaps summarization method instead of truncation would be a better approach
result.append({'file': file, 'content': content[:self.__MAX_LENGTH_OF_FILE]})
result.append({"file": file, "content": content[: self.__MAX_LENGTH_OF_FILE]})
return result
def get_info_from_context(self):
context = []
# TODO: get all keys from context?
for key in ["user_prompt", "chat_history", "Dataset_plan", "Model_plan", "Record_plan",
"Strategy_plan", "Backtest_plan"]:
for key in [
"user_prompt",
"chat_history",
"Dataset_plan",
"Model_plan",
"Record_plan",
"Strategy_plan",
"Backtest_plan",
]:
c = self._context_manager.get_context(key=key)
if c is not None:
c = str(c)

View File

@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from pathlib import Path
DIRNAME = Path(__file__).absolute().resolve().parent

View File

@@ -1,8 +1,9 @@
import json
class Singleton():
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls, *args, **kwargs)

View File

@@ -1,5 +1,7 @@
import sys
import copy
from pathlib import Path
import shutil
from qlib.log import get_module_logger
from qlib.finco.conf import Config
@@ -18,9 +20,7 @@ class WorkflowContextManager:
def set_context(self, key, value):
if key in self.context:
self.logger.warning(
"The key already exists in the context, the value will be overwritten"
)
self.logger.warning("The key already exists in the context, the value will be overwritten")
self.context[key] = value
def get_context(self, key):
@@ -45,14 +45,32 @@ class WorkflowContextManager:
class WorkflowManager:
"""This manange the whole task automation workflow including tasks and actions"""
def __init__(self, name="project", output_path=None) -> None:
if output_path is None:
self._output_path = Path.cwd() / name
def __init__(self, workspace=None) -> None:
if workspace is None:
self._workspace = Path.cwd() / "finco_workspace"
else:
self._output_path = Path(output_path)
self._workspace = Path(workspace)
self._confirm_and_rm()
self._context = WorkflowContextManager()
self._context.set_context("workspace", self._workspace)
self.default_user_prompt = "Please help me build a low turnover strategy that focus more on longterm return in China a stock market. I want to construct a new dataset covers longer history"
def _confirm_and_rm(self):
# if workspace exists, please confirm and remove it. Otherwise exit.
if self._workspace.exists():
flag = input(
f"Will be deleted: "
f"\n\t{self._workspace}"
f"\nIf you do not need to delete {self._workspace}, please change the workspace dir or rename existing files "
f"\nAre you sure you want to delete, yes(Y/y), no (N/n):"
)
if str(flag) not in ["Y", "y"]:
sys.exit()
else:
# remove self._workspace
shutil.rmtree(self._workspace)
def set_context(self, key, value):
"""Direct call set_context method of the context manager"""
self._context.set_context(key, value)
@@ -104,9 +122,8 @@ class WorkflowManager:
if not cfg.continous_mode:
res = t.interact()
t.summarize()
if isinstance(t, WorkflowTask) or isinstance(t, PlanTask) or isinstance(t, ActionTask) \
or isinstance(t, SummarizeTask):
if isinstance(t, (WorkflowTask, PlanTask, ActionTask, SummarizeTask)):
task_list = res + task_list
else:
raise NotImplementedError("Unsupported action type")
return self._output_path
raise NotImplementedError(f"Unsupported Task type {t}")
return self._workspace

View File

@@ -2,9 +2,10 @@
# Requirements
Use following install command to complete the project.
```
pydantic
openai
pip install -e '.[finco]'
```

View File

@@ -173,6 +173,12 @@ setup(
"tianshou<=0.4.10",
"torch",
],
"finco": [
# finco is not necessary for all Qlib users; So a single require section is used for it.
"openapi",
"pydantic", # Please add it to basic requirements after the design of pydantic is state.
"python-dotenv", # I don't think this is necessary if we use pydantic.
],
},
include_package_data=True,
classifiers=[