从自我经验中学习

当 UFO 成功完成一项任务时,用户可以选择保存成功的经验以强化 AppAgent。AppAgent 可以从其自身的成功经验中学习,以提高其未来的性能。

机制

第 1 步:完成会话

  • 事件:UFO 完成一个会话

第 2 步:要求用户保存体验

  • 行动:代理提示用户选择保存成功的经验

Save Experience

第 3 步:用户选择保存

  • 行动:如果用户选择保存经验

第 4 步:总结并保存体验

  • 工具ExperienceSummarizer
  • 过程:
  • 将经验总结为演示示例
  • 将演示示例保存在 config_dev.yaml 文件中指定的 EXPERIENCE_SAVED_PATH
  • 演示示例包含与 AppAgent 提示中使用的字段类似的字段

第 5 步:检索并利用保存的体验

  • :AppAgent 在未来遇到类似任务时
  • 行动:从经验数据库中检索保存的经验
  • 结果:使用检索到的经验生成计划

工作流程图

graph TD;
    A[Complete Session] --> B[Ask User to Save Experience]
    B --> C[User Chooses to Save]
    C --> D[Summarize with ExperienceSummarizer]
    D --> E[Save in EXPERIENCE_SAVED_PATH]
    F[AppAgent Encounters Similar Task] --> G[Retrieve Saved Experience]
    G --> H[Generate Plan]

激活从自我经验中学习

第 1 步:配置 AppAgent

配置以下参数以允许 UFO 使用其自我经验中的 RAG

配置选项 描述 类型 默认值
RAG_EXPERIENCE 是否使用其自我经验中的 RAG 布尔值 False
RAG_EXPERIENCE_RETRIEVED_TOPK 离线检索文档的 topk 整数 5

参考

经验总结器

ExperienceSummarizer 类位于 ufo/experience/experience_summarizer.py 文件中。ExperienceSummarizer 类提供以下方法来总结经验

ExperienceSummarizer 类是经验学习的总结器。

初始化 ApplicationAgentPrompter。

参数
  • is_visual (bool) –

    请求是否针对视觉模型。

  • prompt_template (str) –

    提示模板的路径。

  • example_prompt_template (str) –

    示例提示模板的路径。

  • api_prompt_template (str) –

    API 提示模板的路径。

experience/summarizer.py 中的源代码
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(
    self,
    is_visual: bool,
    prompt_template: str,
    example_prompt_template: str,
    api_prompt_template: str,
):
    """
    Initialize the ApplicationAgentPrompter.
    :param is_visual: Whether the request is for visual model.
    :param prompt_template: The path of the prompt template.
    :param example_prompt_template: The path of the example prompt template.
    :param api_prompt_template: The path of the api prompt template.
    """
    self.is_visual = is_visual
    self.prompt_template = prompt_template
    self.example_prompt_template = example_prompt_template
    self.api_prompt_template = api_prompt_template

build_prompt(log_partition)

构建提示。

参数
  • log_partition (dict) –

    日志分区。返回:提示。

experience/summarizer.py 中的源代码
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def build_prompt(self, log_partition: dict) -> list:
    """
    Build the prompt.
    :param log_partition: The log partition.
    return: The prompt.
    """
    experience_prompter = ExperiencePrompter(
        self.is_visual,
        self.prompt_template,
        self.example_prompt_template,
        self.api_prompt_template,
    )
    experience_system_prompt = experience_prompter.system_prompt_construction()
    experience_user_prompt = experience_prompter.user_content_construction(
        log_partition
    )
    experience_prompt = experience_prompter.prompt_construction(
        experience_system_prompt, experience_user_prompt
    )

    return experience_prompt

create_or_update_vector_db(summaries, db_path) staticmethod

创建或更新向量数据库。

参数
  • summaries (list) –

    总结。

  • db_path (str) –

    向量数据库的路径。

experience/summarizer.py 中的源代码
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@staticmethod
def create_or_update_vector_db(summaries: list, db_path: str):
    """
    Create or update the vector database.
    :param summaries: The summaries.
    :param db_path: The path of the vector database.
    """

    document_list = []

    for summary in summaries:
        request = summary["request"]
        document_list.append(Document(page_content=request, metadata=summary))

    db = FAISS.from_documents(document_list, get_hugginface_embedding())

    # Check if the db exists, if not, create a new one.
    if os.path.exists(db_path):
        prev_db = FAISS.load_local(
            db_path,
            get_hugginface_embedding(),
            allow_dangerous_deserialization=True,
        )
        db.merge_from(prev_db)

    db.save_local(db_path)

    print(f"Updated vector DB successfully: {db_path}")

create_or_update_yaml(summaries, yaml_path) staticmethod

创建或更新 YAML 文件。

参数
  • summaries (list) –

    总结。

  • yaml_path (str) –

    YAML 文件的路径。

experience/summarizer.py 中的源代码
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@staticmethod
def create_or_update_yaml(summaries: list, yaml_path: str):
    """
    Create or update the YAML file.

    :param summaries: The summaries.
    :param yaml_path: The path of the YAML file.
    """

    # Check if the file exists, if not, create a new one
    if not os.path.exists(yaml_path):
        with open(yaml_path, "w"):
            pass
        print(f"Created new YAML file: {yaml_path}")

    # Read existing data from the YAML file
    with open(yaml_path, "r") as file:
        existing_data = yaml.safe_load(file)

    # Initialize index and existing_data if file is empty
    index = len(existing_data) if existing_data else 0
    existing_data = existing_data or {}

    # Update data with new summaries
    for i, summary in enumerate(summaries):
        example = {f"example{index + i}": summary}
        existing_data.update(example)

    # Write updated data back to the YAML file
    with open(yaml_path, "w") as file:
        yaml.safe_dump(
            existing_data, file, default_flow_style=False, sort_keys=False
        )

    print(f"Updated existing YAML file successfully: {yaml_path}")

get_summary(prompt_message)

获取摘要。

参数
  • prompt_message (list) –

    提示消息。返回:摘要和成本。

experience/summarizer.py 中的源代码
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def get_summary(self, prompt_message: list) -> Tuple[dict, float]:
    """
    Get the summary.
    :param prompt_message: The prompt message.
    return: The summary and the cost.
    """

    # Get the completion for the prompt message
    response_string, cost = get_completion(
        prompt_message, "APPAGENT", use_backup_engine=True
    )
    try:
        response_json = json_parser(response_string)
    except:
        response_json = None

    # Restructure the response
    if response_json:
        summary = dict()
        summary["example"] = {}
        for key in [
            "Observation",
            "Thought",
            "ControlLabel",
            "ControlText",
            "Function",
            "Args",
            "Status",
            "Plan",
            "Comment",
        ]:
            summary["example"][key] = response_json.get(key, "")
        summary["Tips"] = response_json.get("Tips", "")

    return summary, cost

get_summary_list(logs)

获取摘要列表。

参数
  • logs (list) –

    日志。返回:摘要列表和总成本。

experience/summarizer.py 中的源代码
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def get_summary_list(self, logs: list) -> Tuple[list, float]:
    """
    Get the summary list.
    :param logs: The logs.
    return: The summary list and the total cost.
    """
    summaries = []
    total_cost = 0.0
    for log_partition in logs:
        prompt = self.build_prompt(log_partition)
        summary, cost = self.get_summary(prompt)
        summary["request"] = log_partition.get("subtask")
        summary["Sub-task"] = log_partition.get("subtask")
        summary["app_list"] = [log_partition.get("application")]
        summaries.append(summary)
        total_cost += cost

    return summaries, total_cost

read_logs(log_path) staticmethod

读取日志。

参数
  • log_path (str) –

    日志文件的路径。

experience/summarizer.py 中的源代码
119
120
121
122
123
124
125
126
@staticmethod
def read_logs(log_path: str) -> list:
    """
    Read the log.
    :param log_path: The path of the log file.
    """
    replay_loader = ExperienceLogLoader(log_path)
    return replay_loader.subtask_partition


经验检索器

ExperienceRetriever 类位于 ufo/rag/retriever.py 文件中。ExperienceRetriever 类提供以下方法来检索经验

基类:Retriever

用于创建经验检索器的类。

创建新的 ExperienceRetriever。

参数
  • db_path

    数据库的路径。

源代码位于 rag/retriever.py
138
139
140
141
142
143
def __init__(self, db_path) -> None:
    """
    Create a new ExperienceRetriever.
    :param db_path: The path to the database.
    """
    self.indexer = self.get_indexer(db_path)

get_indexer(db_path)

创建经验索引器。

参数
  • db_path (str) –

    数据库的路径。

源代码位于 rag/retriever.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def get_indexer(self, db_path: str):
    """
    Create an experience indexer.
    :param db_path: The path to the database.
    """

    try:
        db = FAISS.load_local(
            db_path,
            get_hugginface_embedding(),
            allow_dangerous_deserialization=True,
        )
        return db
    except Exception as e:
        print_with_color(
            "Warning: Failed to load experience indexer from {path}, error: {error}.".format(
                path=db_path, error=e
            ),
            "yellow",
        )
        return None