Learning from Self-Experience

When UFO successfully completes a task, users can save the successful experience to enhance the AppAgent's future performance. The AppAgent learns from its own successful experiences to improve task execution.

Mechanism

graph TD A[Complete Session] --> B[Prompt User to Save Experience] B --> C{User Saves?} C -->|Yes| D[Summarize with ExperienceSummarizer] C -->|No| I[End] D --> E[Save to Experience Database] F[AppAgent Encounters Similar Task] --> G[Retrieve Saved Experience] G --> H[Generate Plan Using Retrieved Experience]

Workflow Steps

  1. Complete a Session: UFO finishes executing a task successfully

  2. Prompt User to Save: The system asks whether to save the experience

    Save Experience Prompt

  3. Summarize Experience: If the user chooses to save, the ExperienceSummarizer processes the session:

  4. Extracts key information from the execution trajectory
  5. Summarizes the experience into a structured demonstration example
  6. Saves it to the experience database at the configured path
  7. The demonstration example includes fields similar to those in the AppAgent's prompt examples

  8. Retrieve and Utilize: When encountering similar tasks in the future:

  9. The AppAgent queries the experience database
  10. Retrieves relevant past experiences
  11. Uses them to inform plan generation

Configuration

Configure the following parameters in config.yaml to enable self-experience learning:

Configuration Option Description Type Default
RAG_EXPERIENCE Enable experience-based learning Boolean False
RAG_EXPERIENCE_RETRIEVED_TOPK Number of top experiences to retrieve Integer 5
EXPERIENCE_SAVED_PATH Database path for storing experiences String "vectordb/experience/"

For more details on RAG configuration, see the RAG Configuration Guide.

API Reference

Experience Summarizer

The ExperienceSummarizer class in ufo/experience/summarizer.py handles experience summarization:

The ExperienceSummarizer class is the summarizer for the experience learning.

Initialize the ApplicationAgentPrompter.

Parameters:
  • is_visual (bool) –

    Whether the request is for visual model.

  • prompt_template (str) –

    The path of the prompt template.

  • example_prompt_template (str) –

    The path of the example prompt template.

  • api_prompt_template (str) –

    The path of the api prompt template.

Source code in 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)

Build the prompt.

Parameters:
  • log_partition (dict) –

    The log partition. return: The prompt.

Source code in 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

Create or update the vector database.

Parameters:
  • summaries (list) –

    The summaries.

  • db_path (str) –

    The path of the vector database.

Source code in 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

Create or update the YAML file.

Parameters:
  • summaries (list) –

    The summaries.

  • yaml_path (str) –

    The path of the YAML file.

Source code in 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)

Get the summary.

Parameters:
  • prompt_message (list) –

    The prompt message. return: The summary and the cost.

Source code in 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)

Get the summary list.

Parameters:
  • logs (list) –

    The logs. return: The summary list and the total cost.

Source code in 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

Read the log.

Parameters:
  • log_path (str) –

    The path of the log file.

Source code in 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

Experience Retriever

The ExperienceRetriever class in ufo/rag/retriever.py handles experience retrieval:

Bases: Retriever

Class to create experience retrievers.

Create a new ExperienceRetriever.

Parameters:
  • db_path

    The path to the database.

Source code in rag/retriever.py
136
137
138
139
140
141
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)

Create an experience indexer.

Parameters:
  • db_path (str) –

    The path to the database.

Source code in rag/retriever.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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:
        logger.warning(
            f"Failed to load experience indexer from {db_path}, error: {e}."
        )
        return None