update
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import quickboard_generator
|
||||
@@ -0,0 +1,7 @@
|
||||
from .json_validator_agent import UserProxyAgentForJsonValidation
|
||||
from .quickboard_ai_generator import QuickboardAiGenerator
|
||||
|
||||
__all__ = [
|
||||
"UserProxyAgentForJsonValidation",
|
||||
"QuickboardAiGenerator"
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from autogen import AssistantAgent, UserProxyAgent, filter_config
|
||||
from textwrap import dedent
|
||||
from prettytable import PrettyTable
|
||||
|
||||
# API_KEY = "_ollama_"
|
||||
# BASE_URL = "http://localhost:11434/v1"
|
||||
|
||||
API_KEY = "_lmstudio_"
|
||||
BASE_URL = "http://localhost:1234/v1"
|
||||
|
||||
LLM_MODEL = "qwen2.5-coder-7b-instruct"
|
||||
|
||||
DEFAULT_AUTOGEN_CONFIG_LIST = [
|
||||
{
|
||||
"model": LLM_MODEL,
|
||||
"base_url": BASE_URL,
|
||||
"api_key": API_KEY,
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_AUTOGEN_LLM_CONFIG = {
|
||||
"config_list": DEFAULT_AUTOGEN_CONFIG_LIST,
|
||||
"cache_seed": None,
|
||||
"temperature": 0.3,
|
||||
"seed": 10
|
||||
}
|
||||
|
||||
QUICKBOARD_BG_COLORS = ["#845ec2","#d65db1","#ff6f91","#ff9671","#ffc75f","#2c73d2","#0081cf","#0089ba","#008e9b","#008f7a"]
|
||||
QUICKBOARD_FG_COLORS = ["#000000", "$ffffff"]
|
||||
|
||||
QUICKBOARD_DATA_UI_JSON_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Generated schema for Root",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": ["basic", "chart", "list"]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"value_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"aggregate_function": {
|
||||
"enum": ["avg", "count", "max", "min", "sum"]
|
||||
},
|
||||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"x_pos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"y_pos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"dimension_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"chart_type": {
|
||||
"enum": ["bar", 'horizontal-bar' "doughnut", "line", "pie", "polar"]
|
||||
},
|
||||
"list_row_limit": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "chart" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["chart_type", "dimension_field"]
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "list" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["list_row_limit"]
|
||||
}
|
||||
},
|
||||
],
|
||||
"required": [
|
||||
"name",
|
||||
"icon",
|
||||
"type",
|
||||
"model",
|
||||
"value_field",
|
||||
"aggregate_function",
|
||||
"x_pos",
|
||||
"y_pos",
|
||||
"width",
|
||||
"height"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
QUICKBOARD_DATA_ONLY_JSON_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Generated schema for Root",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": ["basic", "chart", "list"]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"value_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"aggregate_function": {
|
||||
"enum": ["avg", "count", "max", "min", "sum"]
|
||||
},
|
||||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"dimension_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"chart_type": {
|
||||
"enum": ["bar", "horizontal-bar", "doughnut", "line", "pie", "polar"]
|
||||
},
|
||||
"list_row_limit": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "chart" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["chart_type", "dimension_field"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "basic" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["text_color", "background_color"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "list" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["list_row_limit"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"name",
|
||||
"icon",
|
||||
"type",
|
||||
"model",
|
||||
"value_field",
|
||||
"aggregate_function"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
QUICKBOARD_LAYOUT_JSON_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Generated schema for Root",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": ["basic", "chart", "list"]
|
||||
},
|
||||
"x_pos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"y_pos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type",
|
||||
"x_pos",
|
||||
"y_pos",
|
||||
"width",
|
||||
"height"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
from jsonschema import validate
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Any, Callable, Dict, List, Optional, Literal, Optional, Union
|
||||
|
||||
from autogen import Agent, ConversableAgent
|
||||
from autogen.coding import CodeExecutor, CodeExtractor, MarkdownCodeExtractor, CodeBlock, CodeResult
|
||||
from autogen.runtime_logging import log_new_agent, logging_enabled
|
||||
|
||||
class JsonValidator(CodeExecutor):
|
||||
def __init__(self, json_schema, **kwargs):
|
||||
self.json_schema = json_schema
|
||||
|
||||
@property
|
||||
def code_extractor(self) -> CodeExtractor:
|
||||
return MarkdownCodeExtractor()
|
||||
|
||||
def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CodeResult:
|
||||
logs_all = ""
|
||||
exitcode = 0
|
||||
|
||||
json_code_block_count = 0
|
||||
for idx, code_block in enumerate(code_blocks, start=1):
|
||||
lang, code = code_block.language, code_block.code
|
||||
lang = lang.lower()
|
||||
|
||||
if lang != "json":
|
||||
logs_all += "\n" + f"Skipping execution: language not supported (code block #{idx})."
|
||||
continue
|
||||
|
||||
try:
|
||||
quickboard_json = json.loads(code)
|
||||
validate(quickboard_json, self.json_schema)
|
||||
except Exception as e:
|
||||
exitcode = -1
|
||||
logs_all += f"\nError: {str(e)}"
|
||||
break
|
||||
exitcode = 0
|
||||
logs_all += f"Json is valid."
|
||||
json_code_block_count += 1
|
||||
|
||||
if (exitcode == 0 and json_code_block_count > 0) or exitcode == -1:
|
||||
return CodeResult(exit_code=exitcode, output=logs_all)
|
||||
|
||||
return CodeResult(exit_code=-1, output="Invalid or no json code block was detected, please make sure your code block is marked as json.")
|
||||
|
||||
def restart(self) -> None:
|
||||
self.engine.dispose()
|
||||
|
||||
class UserProxyAgentForJsonValidation(ConversableAgent):
|
||||
DEFAULT_USER_PROXY_AGENT_DESCRIPTIONS = {
|
||||
"ALWAYS": dedent(\
|
||||
"""An attentive HUMAN user who can answer questions about the task, and can perform tasks such as validating json
|
||||
using software tools and reporting back the execution results."""),
|
||||
"TERMINATE": dedent(\
|
||||
"""A user that can validate json using software tools and report back the execution results."""),
|
||||
"NEVER": dedent(\
|
||||
"""An bot that performs no other action than validating json (provided to it's quoted in json blocks)."""),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
json_schema: object,
|
||||
is_termination_msg: Optional[Callable[[Dict], bool]] = None,
|
||||
max_consecutive_auto_reply: Optional[int] = None,
|
||||
human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "NEVER",
|
||||
default_auto_reply: Union[str, Dict] = "",
|
||||
description: Optional[str] = None,
|
||||
):
|
||||
json_validator = JsonValidator(json_schema=json_schema)
|
||||
super().__init__(
|
||||
name=name,
|
||||
is_termination_msg=is_termination_msg,
|
||||
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
||||
human_input_mode=human_input_mode,
|
||||
code_execution_config={"executor": json_validator},
|
||||
default_auto_reply=default_auto_reply,
|
||||
description=(
|
||||
description if description is not None else self.DEFAULT_USER_PROXY_AGENT_DESCRIPTIONS[human_input_mode]
|
||||
),
|
||||
)
|
||||
|
||||
if logging_enabled():
|
||||
log_new_agent(self, locals())
|
||||
|
||||
def run_code(self, code, **kwargs):
|
||||
return -1, "Not supported", None
|
||||
|
||||
def execute_code_blocks(self, code_blocks):
|
||||
return -1, "Not supported"
|
||||
|
||||
def generate_reply(
|
||||
self,
|
||||
messages: Optional[List[Dict[str, Any]]] = None,
|
||||
sender: Optional["Agent"] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, Dict, None]:
|
||||
res = super().generate_reply(messages, sender)
|
||||
|
||||
res_ok = True
|
||||
if isinstance(res, Dict) and len(dict) == 0:
|
||||
res_ok = False
|
||||
elif isinstance(res, str) and str == "":
|
||||
res_ok = False
|
||||
elif res is None:
|
||||
res_ok = False
|
||||
|
||||
if not res_ok:
|
||||
msg = "Invalid or no json code block was detected, please make sure your code block is marked as json."
|
||||
return f"exitcode: -1 (execution failed)\nCode output: {msg}"
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,628 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import copy
|
||||
|
||||
from textwrap import dedent
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from autogen import Agent, AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
|
||||
from autogen.coding import MarkdownCodeExtractor
|
||||
|
||||
from .consts import DEFAULT_AUTOGEN_LLM_CONFIG, QUICKBOARD_FG_COLORS, QUICKBOARD_BG_COLORS, QUICKBOARD_DATA_ONLY_JSON_SCHEMA, QUICKBOARD_LAYOUT_JSON_SCHEMA
|
||||
from .json_validator_agent import UserProxyAgentForJsonValidation
|
||||
|
||||
class QuickboardAiGenerator:
|
||||
_QUICKBOARD_GENERATOR_SYSTEM_MESSAGE = f"""
|
||||
You are an AI assistant specializing in data analysis.
|
||||
Your task is to assist user to build an intuitive and informative dashboard.
|
||||
You help by creating a list dashboard items base on the fields from the given tables.
|
||||
|
||||
Ignore all your previous knowledge about models, here models are the same with relational database tables.
|
||||
Only use the models as mentioned here with common sense, e.g. when the model name is 'sale.order'
|
||||
then the model is about sales orders, etc.
|
||||
|
||||
## DASHBOARD ITEM TYPES
|
||||
There are two kind of dashboard items, 'basic', 'list' and 'chart'.
|
||||
A 'basic' item is used for single value KPI, 'list' items are usualy used to show top (n) data in a tabular list
|
||||
while a 'chart' item is used to show data in a chart.
|
||||
|
||||
Both shared the following common parameters:
|
||||
1. name (string): The title of the item. Required.
|
||||
2. icon (string): Font awesome version 4.7 icon in 'fa-*' format. Required.
|
||||
3. type (string): Dashboard item type. Required.
|
||||
4. model (string): model name, e.g. "sale.order", "product.product", etc. Required.
|
||||
5. value_field (string): Field of the chosen model for the value to be shown, only accept one field. Required.
|
||||
6. aggregate_function (string): Function to be applied on the value field. Required.
|
||||
|
||||
Basic item has the following parameters in addition of the common parameters above:
|
||||
1. text_color (string): Text color for the item. Required.
|
||||
2. background_color (string): Background color for the item. Required.
|
||||
|
||||
Chart item has the following parameters in addition of the common parameters above:
|
||||
1. dimension_field (string): Field of the chosen model for the grouping of the data, only accept one field. Required.
|
||||
2. chart_type (string): The chart type. Required.
|
||||
|
||||
List item has the following parameters in addition of the common parameters above:
|
||||
1. dimension_field (string): Field of the chosen model for the grouping of the data, only accept one field. Required.
|
||||
2. list_row_limit (integer): Number of rows in the list. Required.
|
||||
|
||||
## COLORS
|
||||
Use these color palette for text color parameters: {QUICKBOARD_FG_COLORS}
|
||||
Use these color palette for background color parameters: {QUICKBOARD_BG_COLORS}
|
||||
|
||||
#EXAMPLE
|
||||
Given a model with name 'sale.order' which have the following fields:
|
||||
+-------------+------------------------+-----------+-------------------------+-------------------+
|
||||
| Model | Name | Type | Description | Usage |
|
||||
+-------------+------------------------+-----------+-------------------------+-------------------+
|
||||
| ... | ... | ... | ... | ... |
|
||||
| sale.order | id | integer | ID | value, dimension |
|
||||
| sale.order | date_order | datetime | Order Date | dimension |
|
||||
| sale.order | medium_id | integer | Medium | dimension |
|
||||
| sale.order | sale_order_option_ids | list | Optional Products Lines | |
|
||||
| sale.order | amount_total | monetary | Total | value |
|
||||
| sale.order | amount_to_invoice | monetary | Amount to invoice | value |
|
||||
| ... | ... | ... | ... | ... |
|
||||
+-------------+------------------------+-----------+-------------------------+-------------------+
|
||||
|
||||
And a model with name 'sale.order.line' which have the following fields:
|
||||
+------------------+---------------------------+-----------+--------------------------------+-------------------+
|
||||
| Model | Name | Type | Description | Usage |
|
||||
+------------------+---------------------------+-----------+--------------------------------+-------------------+
|
||||
| ... | ... | ... | ... | ... |
|
||||
| sale.order.line | product_id | integer | Product | value, dimension |
|
||||
| sale.order.line | product_uom_qty | float | Quantity | value |
|
||||
| sale.order.line | qty_delivered_method | char | Method to update delivered qty | value, dimension |
|
||||
| sale.order.line | qty_delivered | float | Delivery Quantity | value |
|
||||
| ... | ... | ... | ... | ... |
|
||||
+------------------+---------------------------+-----------+--------------------------------+-------------------+
|
||||
|
||||
You may choose to answer as follow:
|
||||
```json
|
||||
[
|
||||
{{
|
||||
\"name\": \"Sales Order Count\",
|
||||
\"icon\": \"fa-shopping-bag\",
|
||||
\"type\": \"basic\",
|
||||
\"model\": \"sale.order\",
|
||||
\"value_field\": \"id\",
|
||||
\"aggregate_function\": \"count\",
|
||||
\"text_color\": \"#000000\",
|
||||
\"background_color\": \"#FFFFFF\"
|
||||
}},
|
||||
{{
|
||||
\"name\": \"Sales Order Total\",
|
||||
\"icon\": \"fa-shopping-cart\",
|
||||
\"type\": \"basic\",
|
||||
\"model\": \"sale.order\",
|
||||
\"value_field\": \"amount_total\",
|
||||
\"aggregate_function\": \"sum\",
|
||||
\"text_color\": \"#000000\",
|
||||
\"background_color\": \"#3b3b3b\"
|
||||
}},
|
||||
{{
|
||||
\"name\": \"Total Amount By Date\",
|
||||
\"icon\": \"fa-usd\",
|
||||
\"type\": \"chart\",
|
||||
\"model\": \"sale.order\",
|
||||
\"value_field\": \"amount_total\",
|
||||
\"aggregate_function\": \"sum\",
|
||||
\"dimension_field\": \"date_order\",
|
||||
\"datetime_granularity\": \"day\",
|
||||
\"chart_type\": \"line\"
|
||||
}},
|
||||
{{
|
||||
\"name\": \"Top 10 Products\",
|
||||
\"icon\": \"fa-shopping-bag\",
|
||||
\"type\": \"list\",
|
||||
\"model\": \"sale.order.line\",
|
||||
\"value_field\": \"product_uom_qty\",
|
||||
\"aggregate_function\": \"sum\",
|
||||
\"dimension_field\": \"product_id\",
|
||||
\"list_row_limit\": 10
|
||||
}}
|
||||
]
|
||||
```
|
||||
|
||||
## RULES
|
||||
1. Answer only with the definitions of the items in a json list fenced in json code block.
|
||||
2. Do not comment. Do not explain your answer.
|
||||
3. Do not use models other than the user specifies.
|
||||
4. Pay attention to which field belong to which model. Do not to use fields from other models.
|
||||
5. Treat each model independently, do not mix fields from one model to another.
|
||||
6. All parameters are required, never set any parameter to null.
|
||||
7. Set parameter 'type' to 'basic' for basic items, set it to 'list' for list items and set it to 'chart' for chart items.
|
||||
8. Parameter 'chart_type' must be one of ['bar', 'horizontal-bar', 'doughnut', 'line', 'pie', 'polar'].
|
||||
9. Use one of ['avg', 'count', 'max', 'min', 'sum'] for 'aggregate_function' if the 'value_field' is one of ['integer', 'float', 'monetary'].
|
||||
10. For other types of 'value_field' such as 'char', 'many2one', etc., the parameter 'aggregate_function' must only be 'count'.
|
||||
11. Never use field with type 'date' or 'datetime' for 'value_field'.
|
||||
12. Parameter 'value_field' and 'dimension_field', requires exact field name, use the field as is without prefixes nor suffixes.
|
||||
13. If the type of 'dimension_field' is date or datetime, you may add 'datetime_granularity' parameter to specify the precision.
|
||||
'datetime_granularity' must be one of ["year", "month", "day"].
|
||||
14. Do not assume a field has relation to other model, so again, 'value_field' and 'dimension_field' must use the exact name as mentioned here.
|
||||
15. Use only single color for color related parameters, not an array of colors, choose one of the colors mentioned above.
|
||||
16. Fence your anwser with markdown json code block (```json your_answer ```).
|
||||
17. Your answer will be validated by a bot, if your answer is not valid then you must fix it.
|
||||
18. When fixing answer re-evaluate everything and do not give comment on the json code block as it will create another error.
|
||||
19. When fixing answer always reply with the the fixed json code block with every items.
|
||||
"""
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
|
||||
self._admin: UserProxyAgent = None
|
||||
self._quickboard_ai: AssistantAgent = None
|
||||
self._json_validator: UserProxyAgentForJsonValidation = None
|
||||
self._groupchat: GroupChat = None
|
||||
self._manager: GroupChatManager = None
|
||||
|
||||
def _create_agents(self, data_json_schema):
|
||||
admin = UserProxyAgent(
|
||||
"admin",
|
||||
description="The user who give tasks and questions.",
|
||||
human_input_mode="NEVER",
|
||||
is_termination_msg=lambda message: True, # Always True
|
||||
code_execution_config=False,
|
||||
)
|
||||
|
||||
quickboard_generator = AssistantAgent(
|
||||
name="quickboard_generator",
|
||||
description=f"AI that generate dashboards.",
|
||||
system_message=dedent(self._QUICKBOARD_GENERATOR_SYSTEM_MESSAGE),
|
||||
human_input_mode="NEVER",
|
||||
llm_config=DEFAULT_AUTOGEN_LLM_CONFIG,
|
||||
)
|
||||
|
||||
data_json_validator = UserProxyAgentForJsonValidation(
|
||||
"data_json_validator",
|
||||
json_schema=data_json_schema,
|
||||
description="An bot that performs no other action than validating json (provided to it's quoted in json blocks).",
|
||||
human_input_mode="NEVER",
|
||||
)
|
||||
|
||||
def _speaker_selection_func(last_speaker: Agent, groupchat: GroupChat):
|
||||
last_messages = groupchat.messages[-1]
|
||||
next_speaker = admin
|
||||
|
||||
if last_speaker is admin:
|
||||
next_speaker = quickboard_generator
|
||||
elif last_speaker is quickboard_generator:
|
||||
next_speaker = data_json_validator
|
||||
elif last_speaker is data_json_validator:
|
||||
# if last agent reply with invalid json then let it try again
|
||||
if last_messages["content"].find("exitcode: -1") > -1:
|
||||
next_speaker = quickboard_generator
|
||||
elif last_messages["content"].strip() == "":
|
||||
next_speaker = quickboard_generator
|
||||
|
||||
return next_speaker
|
||||
|
||||
groupchat = GroupChat(
|
||||
agents=[admin, quickboard_generator, data_json_validator],
|
||||
messages=[],
|
||||
max_round=5,
|
||||
speaker_selection_method= _speaker_selection_func,
|
||||
# send_introductions=True,
|
||||
)
|
||||
|
||||
manager = GroupChatManager(
|
||||
groupchat=groupchat,
|
||||
name="chat_manager",
|
||||
llm_config=DEFAULT_AUTOGEN_LLM_CONFIG
|
||||
)
|
||||
|
||||
return (admin, quickboard_generator, data_json_validator, manager, groupchat)
|
||||
|
||||
def _build_agent_parameters(self, models):
|
||||
model_names = []
|
||||
model_infos = []
|
||||
|
||||
data_json_schema = copy.deepcopy(QUICKBOARD_DATA_ONLY_JSON_SCHEMA)
|
||||
|
||||
for model in models:
|
||||
field_defs = PrettyTable()
|
||||
field_defs.align = "l"
|
||||
field_defs.field_names = ["Model", "Name", "Type", "Description", "Usage"]
|
||||
|
||||
fields = self.env[model.model].fields_get()
|
||||
|
||||
valid_value_fields = []
|
||||
valid_dimension_fields = []
|
||||
|
||||
for k, v in fields.items():
|
||||
if v["store"]:
|
||||
|
||||
if v["type"] in ["many2many", "one2many"]:
|
||||
field_type = "list"
|
||||
elif v["type"] == "selection":
|
||||
field_type = "char"
|
||||
elif v["type"] == "many2one":
|
||||
field_type = "integer"
|
||||
else:
|
||||
field_type = v["type"]
|
||||
|
||||
usage = []
|
||||
if v["type"] not in ["many2many", "one2many", "float", "monetary"]:
|
||||
valid_dimension_fields.append(f"{k}")
|
||||
usage.append("dimension")
|
||||
|
||||
if v["type"] in ["integer", "float", "monetary", "many2one", "selection"]:
|
||||
valid_value_fields.append(f"{k}")
|
||||
usage.append("value")
|
||||
|
||||
field_defs.add_row([model.model, f"{k}", field_type, v['string'], ",".join(usage)])
|
||||
|
||||
valid_value_fields_str = ", ".join([f"'{o}'" for o in valid_value_fields])
|
||||
valid_dimension_fields_str = ", ".join([f"'{o}'" for o in valid_dimension_fields])
|
||||
|
||||
model_names.append(model.model)
|
||||
model_infos.append({
|
||||
"name": model.model,
|
||||
"field_defs": field_defs.get_string(),
|
||||
"value_fields": valid_value_fields_str,
|
||||
"dimension_fields": valid_dimension_fields_str
|
||||
})
|
||||
|
||||
model_name_schema = {
|
||||
"const": model.model
|
||||
}
|
||||
|
||||
value_field_schema = {
|
||||
"if": {
|
||||
"properties": {
|
||||
"model": model_name_schema
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"value_field": {
|
||||
"enum": valid_value_fields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dimension_field_schema = {
|
||||
"if": {
|
||||
"properties": {
|
||||
"model": model_name_schema
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"dimension_field": {
|
||||
"enum": valid_dimension_fields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data_json_schema["items"]["model"] = model_name_schema
|
||||
data_json_schema["items"]["allOf"].append(value_field_schema)
|
||||
data_json_schema["items"]["allOf"].append(dimension_field_schema)
|
||||
|
||||
return model_names, model_infos, data_json_schema
|
||||
|
||||
def _create_message(self, model_names, model_infos):
|
||||
message = dedent(f"""
|
||||
Create dashboard items from these models {[o for o in model_names]}.
|
||||
""")
|
||||
|
||||
for mi in model_infos:
|
||||
mi_string = dedent(f"""
|
||||
### MODEL '{mi["name"]}'
|
||||
The model '{mi["name"]}' has the following fields:
|
||||
{mi["field_defs"]}
|
||||
""")
|
||||
|
||||
message = message + "\n" + mi_string
|
||||
|
||||
|
||||
message += dedent("""
|
||||
Pay attention to the field usage. Valid values for 'value_field' and 'dimension_field' are based on it.
|
||||
|
||||
Also pay attention to which model a field belongs to, do not use other models field in a dashboard item.
|
||||
Always use the exact field name as mentioned above on the models field.
|
||||
|
||||
Create at least 3 'basic' items, 3 'list' items and 3 'chart' items.
|
||||
Make the 'basic' items background colorful with matching but still readable text color.
|
||||
Answer only in a json list fenced in a json code block.
|
||||
""")
|
||||
|
||||
return message
|
||||
|
||||
def _arrange_items(self, quickboard_items):
|
||||
items = copy.deepcopy(quickboard_items)
|
||||
basic_items = [o for o in items if o["type"] == "basic"]
|
||||
list_chart_items = [o for o in items if o["type"] != "basic"]
|
||||
|
||||
fin = []
|
||||
row = 0
|
||||
while len(basic_items) > 0:
|
||||
count = 1
|
||||
width = 6
|
||||
if len(basic_items) >= 6:
|
||||
count = 6
|
||||
width = 2
|
||||
elif len(basic_items) >= 4:
|
||||
count = 4
|
||||
width = 3
|
||||
elif len(basic_items) >= 3:
|
||||
count = 3
|
||||
width = 4
|
||||
elif len(basic_items) >= 2:
|
||||
count = 2
|
||||
width = 6
|
||||
|
||||
row_items = basic_items[:count]
|
||||
for k, o in enumerate(row_items):
|
||||
o["y_pos"] = row
|
||||
o["x_pos"] = k * width
|
||||
o["height"] = 1
|
||||
o["width"] = width
|
||||
|
||||
fin.extend(row_items)
|
||||
del basic_items[:count]
|
||||
|
||||
row += 1
|
||||
|
||||
while len(list_chart_items) > 0:
|
||||
width = 4
|
||||
count = 3
|
||||
|
||||
if len(list_chart_items) >= 4:
|
||||
count = 4
|
||||
width = 3
|
||||
elif len(list_chart_items) >= 3:
|
||||
count = 3
|
||||
width = 4
|
||||
elif len(list_chart_items) >= 2:
|
||||
count = 2
|
||||
width = 6
|
||||
|
||||
row_items = list_chart_items[:count]
|
||||
for k, o in enumerate(row_items):
|
||||
o["y_pos"] = row
|
||||
o["x_pos"] = k * width
|
||||
o["height"] = 2
|
||||
o["width"] = width
|
||||
|
||||
fin.extend(row_items)
|
||||
del list_chart_items[:count]
|
||||
|
||||
row += 2
|
||||
|
||||
return fin
|
||||
|
||||
def generate_quickboard(self, models, layout_by_ai, screen_w, screen_h, cell_w, cell_h):
|
||||
model_names, model_infos, json_schema = self._build_agent_parameters(models)
|
||||
|
||||
self._admin,\
|
||||
self._quickboard_ai,\
|
||||
self._json_validator,\
|
||||
self._manager,\
|
||||
self._groupchat = self._create_agents(json_schema)
|
||||
|
||||
message = self._create_message(model_names, model_infos)
|
||||
answer = self._admin.initiate_chat(self._manager, message=message) #, silent=True)
|
||||
|
||||
res = ""
|
||||
|
||||
# The json is not on answer.summary / last message since the last agent is json validator when successful
|
||||
if answer.summary.find("exitcode: 0") > -1:
|
||||
extractor = MarkdownCodeExtractor()
|
||||
code_blocks = extractor.extract_code_blocks(answer.chat_history[-2]["content"])
|
||||
if len(code_blocks) > 0:
|
||||
quickboard = code_blocks[0].code
|
||||
quickboard_items = json.loads(quickboard)
|
||||
|
||||
arranged_items = []
|
||||
if layout_by_ai:
|
||||
aiDesigner = QuickboardAIDesigner()
|
||||
arranged_items = aiDesigner.arrange_items_with_ai(quickboard_items, screen_w, screen_h, cell_w, cell_h)
|
||||
else:
|
||||
arranged_items = self._arrange_items(quickboard_items)
|
||||
|
||||
res = json.dumps(arranged_items)
|
||||
|
||||
return res
|
||||
|
||||
class QuickboardAIDesigner:
|
||||
_QUICKBOARD_DESIGNER_SYSTEM_MESSAGE = f"""
|
||||
Your task is to design the layout of a dashboard from the given dashboard items.
|
||||
|
||||
## DASHBOARD ITEM TYPES
|
||||
There are three kind of dashboard items, basic, list and chart.
|
||||
A basic item is used for single value KPI, a list is used to display data in a tabular list
|
||||
while a chart item is used to show data in a graphical chart.
|
||||
|
||||
All have the following attribute:
|
||||
1. id: The id of the dashboard item.
|
||||
2. type: Dashboard item type, 'basic' for basic items, 'list' for list items and 'chart' for chart items.
|
||||
3. x_pos: The horizontal position on the grid (0 to 11, representing the block's position).
|
||||
4. y_pos: The vertical position on the grid (measured in blocks, can be unlimited).
|
||||
5. width: The width of the item.
|
||||
6. height: The height of the item.
|
||||
|
||||
## LAYOUT
|
||||
The dashboard layout is a grid system measured in square blocks.
|
||||
|
||||
It has 12 blocks for column width and unlimited cell rows.
|
||||
The origin (0, 0) position is on the top left. The maximum position of the top row is (12, 0).
|
||||
|
||||
## REQUIREMENTS
|
||||
1. Prioritize the arrangement of blocks to optimize space, i,e. no gaps between items.
|
||||
2. Ensure that no blocks overlap and that all blocks are positioned within the defined grid limits.
|
||||
3. Arrange the 'basic' items to fill the top rows.
|
||||
4. Arrange the rest of the items to fill the rows after the 'basic' items.
|
||||
|
||||
## RULES
|
||||
1. Do not change the 'id' and the 'type' of the dashboard items,
|
||||
i.e, the 'id' and 'type' is a fixed pair. If you want to re-arrange the items, always use the same 'id' and 'type' pair.
|
||||
2. Only edit these attribute 'x_pos', 'y_pos', 'width', 'height'.
|
||||
3. Do not add nor remove dashboard items!
|
||||
4. Fence your anwser with markdown json block.
|
||||
5. Do not comment. Do not explain your answer.
|
||||
6. Your answer will be validated by a bot, if your answer is not valid then you must fix it.
|
||||
7. When fixing answer re-evaluate everything and do not give comment as it will create another error.
|
||||
8. When fixing answer always reply with the the fixed json code block with every items.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._admin: UserProxyAgent = None
|
||||
self._quickboard_ai: AssistantAgent = None
|
||||
self._json_validator: UserProxyAgentForJsonValidation = None
|
||||
self._groupchat: GroupChat = None
|
||||
self._manager: GroupChatManager = None
|
||||
|
||||
def _create_agents(self, ui_json_schema):
|
||||
admin = UserProxyAgent(
|
||||
"admin",
|
||||
description="The user who give tasks and questions.",
|
||||
human_input_mode="NEVER",
|
||||
is_termination_msg=lambda message: True, # Always True
|
||||
code_execution_config=False,
|
||||
)
|
||||
|
||||
quickboard_designer = AssistantAgent(
|
||||
name="quickboard_designer",
|
||||
description=f"AI that design the layout of dashboards.",
|
||||
system_message=dedent(self._QUICKBOARD_DESIGNER_SYSTEM_MESSAGE),
|
||||
human_input_mode="NEVER",
|
||||
llm_config=DEFAULT_AUTOGEN_LLM_CONFIG,
|
||||
)
|
||||
|
||||
ui_json_validator = UserProxyAgentForJsonValidation(
|
||||
"ui_json_validator",
|
||||
json_schema=ui_json_schema,
|
||||
description="An bot that performs no other action than validating json (provided to it's quoted in json blocks).",
|
||||
human_input_mode="NEVER",
|
||||
)
|
||||
|
||||
def _speaker_selection_func(last_speaker: Agent, groupchat: GroupChat):
|
||||
last_messages = groupchat.messages[-1]
|
||||
next_speaker = admin
|
||||
|
||||
if last_speaker is admin:
|
||||
next_speaker = quickboard_designer
|
||||
elif last_speaker is quickboard_designer:
|
||||
next_speaker = ui_json_validator
|
||||
elif last_speaker is ui_json_validator:
|
||||
# if last agent reply with invalid json then let it try again
|
||||
if last_messages["content"].find("exitcode: -1") > -1:
|
||||
next_speaker = quickboard_designer
|
||||
elif last_messages["content"].strip() == "":
|
||||
next_speaker = quickboard_designer
|
||||
|
||||
return next_speaker
|
||||
|
||||
groupchat = GroupChat(
|
||||
agents=[admin, quickboard_designer, ui_json_validator],
|
||||
messages=[],
|
||||
max_round=5,
|
||||
speaker_selection_method= _speaker_selection_func,
|
||||
# send_introductions=True,
|
||||
)
|
||||
|
||||
manager = GroupChatManager(
|
||||
groupchat=groupchat,
|
||||
name="chat_manager",
|
||||
llm_config=DEFAULT_AUTOGEN_LLM_CONFIG
|
||||
)
|
||||
|
||||
return (admin, quickboard_designer, ui_json_validator, manager, groupchat)
|
||||
|
||||
def _build_agent_parameters(self, quickboard_items):
|
||||
layout_json_schema = copy.deepcopy(QUICKBOARD_LAYOUT_JSON_SCHEMA)
|
||||
|
||||
item_count = len(quickboard_items)
|
||||
item_count_schema = {
|
||||
"minItems": item_count,
|
||||
"maxItems": item_count
|
||||
}
|
||||
|
||||
layout_json_schema.update(item_count_schema)
|
||||
|
||||
item_id_type_schema = []
|
||||
for item in quickboard_items:
|
||||
id_type_schema = {
|
||||
"if": {
|
||||
"properties": {
|
||||
"id": { "const": item["id"] }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"type": { "const": item["type"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
item_id_type_schema.append(id_type_schema)
|
||||
|
||||
layout_json_schema["items"]["allOf"] = item_id_type_schema
|
||||
|
||||
return layout_json_schema
|
||||
|
||||
def _create_message(self, quickboard_items, screen_w, screen_h, cell_w, cell_h):
|
||||
layout_items = [[{
|
||||
"id": o["id"],
|
||||
"type": o["type"],
|
||||
"x_pos": 0,
|
||||
"y_pos": 0,
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}] for o in quickboard_items]
|
||||
|
||||
layout_items_count = len(layout_items)
|
||||
layout_items_str = ", ".join([ f"{json.dumps(o)}\n" for o in layout_items])
|
||||
|
||||
message = f"""
|
||||
Design dashboard layout for these {layout_items_count} items: {layout_items_str}.
|
||||
|
||||
The cell size for my screen is {cell_w} x {cell_h} (width x height).
|
||||
My screen size is {screen_w} x {screen_h} (width x height).
|
||||
|
||||
Make it compact, for 'basic' items 1 block for its height is enough.
|
||||
For 'list' and 'chart' items 3 blocks for its height are enough.
|
||||
|
||||
Put basic items first then list and chart items.
|
||||
Do not change the 'id' and the 'type' of the items.
|
||||
Remember there are {layout_items_count}, use them all, do not add any item nor remove any of them.
|
||||
"""
|
||||
return message
|
||||
|
||||
def arrange_items_with_ai(self, quickboard_items, screen_w, screen_h, cell_w, cell_h):
|
||||
res = quickboard_items
|
||||
for i, item in enumerate(quickboard_items, start=1):
|
||||
item["id"] = i
|
||||
|
||||
json_schema = self._build_agent_parameters(quickboard_items)
|
||||
|
||||
self._admin,\
|
||||
self._quickboard_ai,\
|
||||
self._json_validator,\
|
||||
self._manager,\
|
||||
self._groupchat = self._create_agents(json_schema)
|
||||
|
||||
message = self._create_message(quickboard_items, screen_w, screen_h, cell_w, cell_h)
|
||||
answer = self._admin.initiate_chat(self._manager, message=message) #, silent=True)
|
||||
|
||||
if answer.summary.find("exitcode: 0") > -1:
|
||||
extractor = MarkdownCodeExtractor()
|
||||
code_blocks = extractor.extract_code_blocks(answer.chat_history[-2]["content"])
|
||||
if len(code_blocks) > 0:
|
||||
layout = code_blocks[0].code
|
||||
layout_items = json.loads(layout)
|
||||
|
||||
# merge item and layout
|
||||
fin_items = []
|
||||
for item in quickboard_items:
|
||||
for layout in layout_items:
|
||||
if layout["id"] == item["id"] and layout["type"] == item["type"]:
|
||||
item.update(layout)
|
||||
break
|
||||
|
||||
fin_items.append(item)
|
||||
|
||||
return fin_items
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
import json
|
||||
from jsonschema import validate
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from .ai import QuickboardAiGenerator
|
||||
from .ai.consts import QUICKBOARD_DATA_UI_JSON_SCHEMA, QUICKBOARD_BG_COLORS, QUICKBOARD_FG_COLORS
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class QuickboardGenerator(models.TransientModel):
|
||||
_name = "quickboard.generator"
|
||||
_description = "Generate quickbaord items with AI."
|
||||
|
||||
model_ids = fields.Many2many('ir.model', string='Model')
|
||||
layout_by_ai = fields.Boolean("Layout by AI", default=False)
|
||||
|
||||
def action_generate_quickboard(self):
|
||||
if len(self.model_ids.ids) < 1:
|
||||
return {
|
||||
'name': _('Generate Quickboard'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'quickboard.generator',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
screen_width = self.env.context.get("screen_width")
|
||||
screen_height = self.env.context.get("screen_height")
|
||||
cell_width = self.env.context.get("cell_width")
|
||||
cell_height = self.env.context.get("cell_height")
|
||||
|
||||
try:
|
||||
gen = QuickboardAiGenerator(self.env)
|
||||
quickboard = gen.generate_quickboard(
|
||||
self.model_ids,
|
||||
self.layout_by_ai,
|
||||
screen_width,
|
||||
screen_height,
|
||||
cell_width,
|
||||
cell_height
|
||||
)
|
||||
quickboard_json = json.loads(quickboard)
|
||||
validate(quickboard_json, QUICKBOARD_DATA_UI_JSON_SCHEMA)
|
||||
|
||||
quickboard_item = self.env['quickboard.item']
|
||||
items = quickboard_item.search([])
|
||||
for o in items:
|
||||
o.unlink()
|
||||
|
||||
for o in quickboard_json:
|
||||
_logger.info(f"Creating quickboard item: {o}")
|
||||
|
||||
model = self.env["ir.model"].search([("model", "=", o["model"])])
|
||||
value_field = self.env["ir.model.fields"].search([("model_id", "=", model.id), ("name", "=", o["value_field"])])
|
||||
|
||||
if not value_field.id:
|
||||
raise Exception("AI generated invalid field: %s." % {o["value_field"]})
|
||||
|
||||
# Sometime AI choose the wrong aggregate function, we could return the result to AI with json schema validation.
|
||||
# But, it would mean failing the result and making another attempt, the alternative is we just fix it here.
|
||||
if value_field.ttype not in ['float', 'integer', 'monetary'] and o["aggregate_function"] != "count":
|
||||
o["aggregate_function"] = "count"
|
||||
|
||||
vals = {
|
||||
"name": o["name"],
|
||||
"model_id": model.id,
|
||||
"icon": o["icon"],
|
||||
"type": o["type"],
|
||||
"value_field_id": [value_field.id],
|
||||
"aggregate_function": o["aggregate_function"],
|
||||
"x_pos": o["x_pos"],
|
||||
"y_pos": o["y_pos"],
|
||||
"height": o["height"],
|
||||
"width": o["width"]
|
||||
}
|
||||
if o["type"] == "basic":
|
||||
text_color = 0
|
||||
if o["text_color"] and o["text_color"] in QUICKBOARD_FG_COLORS:
|
||||
text_color = QUICKBOARD_FG_COLORS.index(o["text_color"])
|
||||
|
||||
back_color = 0
|
||||
if o["text_color"] and o["text_color"] in QUICKBOARD_BG_COLORS:
|
||||
back_color = QUICKBOARD_BG_COLORS.index(o["text_color"])
|
||||
|
||||
vals.update({
|
||||
"text_color": text_color,
|
||||
"background_color": back_color,
|
||||
})
|
||||
elif o["type"] == "chart":
|
||||
dimension_field = self.env["ir.model.fields"].search([("model_id", "=", model.id), ("name", "=", o["dimension_field"])])
|
||||
if not dimension_field.id:
|
||||
raise Exception("AI generated invalid field: %s." % {o["dimension_field"]})
|
||||
|
||||
vals.update({
|
||||
"dimension_field_id": dimension_field.id,
|
||||
"chart_type": o["chart_type"],
|
||||
})
|
||||
|
||||
if "datetime_granularity" in o:
|
||||
vals.update({
|
||||
"datetime_granularity": o["datetime_granularity"]
|
||||
})
|
||||
elif o["type"] == "list":
|
||||
dimension_field = self.env["ir.model.fields"].search([("model_id", "=", model.id), ("name", "=", o["dimension_field"])])
|
||||
if not dimension_field.id:
|
||||
raise Exception("AI generated invalid field: %s." % {o["dimension_field"]})
|
||||
|
||||
vals.update({
|
||||
"dimension_field_id": dimension_field.id,
|
||||
"list_row_limit": o["list_row_limit"]
|
||||
})
|
||||
|
||||
if "datetime_granularity" in o:
|
||||
vals.update({
|
||||
"datetime_granularity": o["datetime_granularity"]
|
||||
})
|
||||
|
||||
quickboard_item.with_context(ai_generation=True).create(vals)
|
||||
except Exception as e:
|
||||
_logger.error("Error generating quickboard", e)
|
||||
raise ValidationError(_("Unfortunately the AI didn't generate valid quickboard. Please try again."))
|
||||
|
||||
for rec in self:
|
||||
self.env["bus.bus"]._sendone("quickboard", "quickboard_updated", {})
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="generate_quickboard_view" model="ir.ui.view">
|
||||
<field name="name">quickboard.generator.view</field>
|
||||
<field name="model">quickboard.generator</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="fa fa-exclamation-triangle"/> All quickboard items will be removed.
|
||||
</div>
|
||||
<group>
|
||||
<field name="model_ids"
|
||||
options="{'no_create_edit':True,'no_create': True}"
|
||||
domain="[('transient', '=', False)]"
|
||||
widget="many2many_tags"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="layout_by_ai" />
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="not layout_by_ai">
|
||||
<i class="fa fa-info-circle"/> The result might not what you've expected.
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_generate_quickboard" string="Ok" type="object" default_focus="1" class="oe_highlight"/>
|
||||
<button string="Cancel" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user