chore: import upstream snapshot with attribution
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:52 +08:00
commit e768098d0e
4004 changed files with 2804145 additions and 0 deletions
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-4
@@ -0,0 +1 @@
{"name": "FilmTriviaGPT", "role": "an AI specialized in film trivia that provides accurate and up-to-date information about movies, directors, actors, and more.", "goals": ["Introduce 'Lord of the Rings' film trilogy including the film title, release year, director, current age of the director, production company and a brief summary of the film."]}
@@ -0,0 +1,127 @@
import sys
from io import StringIO
import functools
import logging
import ast
from typing import Dict, Optional
logger = logging.getLogger(__name__)
@functools.lru_cache(maxsize=None)
def warn_once() -> None:
# Warn that the PythonREPL
logger.warning("Python REPL can execute arbitrary code. Use with caution.")
COMMAND_EXECUTION_FUNCTIONS = ["system", "exec", "execfile", "eval"]
class PythonValidation:
def __init__(
self,
allow_imports: bool = False,
allow_command_exec: bool = False,
):
"""Initialize a PALValidation instance.
Args:
allow_imports (bool): Allow import statements.
allow_command_exec (bool): Allow using known command execution functions.
"""
self.allow_imports = allow_imports
self.allow_command_exec = allow_command_exec
def validate_code(self, code: str) -> None:
try:
code_tree = ast.parse(code)
except (SyntaxError, UnicodeDecodeError):
raise ValueError(f"Generated code is not valid python code: {code}")
except TypeError:
raise ValueError(
f"Generated code is expected to be a string, "
f"instead found {type(code)}"
)
except OverflowError:
raise ValueError(
f"Generated code too long / complex to be parsed by ast: {code}"
)
has_imports = False
top_level_nodes = list(ast.iter_child_nodes(code_tree))
for node in top_level_nodes:
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
has_imports = True
if not self.allow_imports and has_imports:
raise ValueError(f"Generated code has disallowed imports: {code}")
if (
not self.allow_command_exec
or not self.allow_imports
):
for node in ast.walk(code_tree):
if (
(not self.allow_command_exec)
and isinstance(node, ast.Call)
and (
(
hasattr(node.func, "id")
and node.func.id in COMMAND_EXECUTION_FUNCTIONS
)
or (
isinstance(node.func, ast.Attribute)
and node.func.attr in COMMAND_EXECUTION_FUNCTIONS
)
)
):
raise ValueError(
f"Found illegal command execution function "
f"{node.func.id} in code {code}"
)
if (not self.allow_imports) and (
isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom)
):
raise ValueError(f"Generated code has disallowed imports: {code}")
class PythonREPL:
"""Simulates a standalone Python REPL."""
def __init__(self) -> None:
self.globals: Optional[Dict] = globals()
self.locals: Optional[Dict] = None
self.code_validations = PythonValidation(allow_imports=True)
def run(self, command: str) -> str:
"""Run command with own globals/locals and returns anything printed."""
# Warn against dangers of PythonREPL
warn_once()
self.code_validations.validate_code(command)
old_stdout = sys.stdout
sys.stdout = my_stdout = StringIO()
try:
exec(command, self.globals, self.locals)
sys.stdout = old_stdout
output = my_stdout.getvalue()
except Exception as e:
sys.stdout = old_stdout
output = repr(e)
print(output)
return output
python_repl = PythonREPL()
def python(command: str):
"""
A Python shell. Use this to execute python commands. Input should be a valid python command.
If you want to see the output of a value, you should print it out with `print(...)`.
"""
command = command.strip().strip("```")
return python_repl.run(command)
@@ -0,0 +1,5 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
beautifulsoup4
requests
@@ -0,0 +1,27 @@
import asyncio
from workflow import AutoGPTInput, create_workflow
async def main():
print("--- Running autonomous agent ---")
workflow = create_workflow()
result = await workflow.run(
AutoGPTInput(
name="FilmTriviaGPT",
goals=[
"Introduce 'Lord of the Rings' film trilogy including the film title, "
"release year, director, current age of the director, production company "
"and a brief summary of the film."
],
role=(
"an AI specialized in film trivia that provides accurate and up-to-date "
"information about movies, directors, actors, and more."
),
)
)
print(f"Output:\n{result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,62 @@
from bs4 import BeautifulSoup
import re
import requests
def decode_str(string):
return string.encode().decode("unicode-escape").encode("latin1").decode("utf-8")
def get_page_sentence(page, count: int = 10):
# find all paragraphs
paragraphs = page.split("\n")
paragraphs = [p.strip() for p in paragraphs if p.strip()]
# find all sentence
sentences = []
for p in paragraphs:
sentences += p.split('. ')
sentences = [s.strip() + '.' for s in sentences if s.strip()]
# get first `count` number of sentences
return ' '.join(sentences[:count])
def remove_nested_parentheses(string):
pattern = r'\([^()]+\)'
while re.search(pattern, string):
string = re.sub(pattern, '', string)
return string
def search(entity: str, count: int = 10):
"""
The input is an exact entity name. The action will search this entity name on Wikipedia and returns the first
count sentences if it exists. If not, it will return some related entities to search next.
"""
entity_ = entity.replace(" ", "+")
search_url = f"https://en.wikipedia.org/w/index.php?search={entity_}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35"
}
response_text = requests.get(search_url, headers=headers).text
soup = BeautifulSoup(response_text, features="html.parser")
result_divs = soup.find_all("div", {"class": "mw-search-result-heading"})
if result_divs: # mismatch
result_titles = [decode_str(div.get_text().strip()) for div in result_divs]
result_titles = [remove_nested_parentheses(result_title) for result_title in result_titles]
obs = f"Could not find {entity}. Similar: {result_titles[:5]}."
else:
page_content = [p_ul.get_text().strip() for p_ul in soup.find_all("p") + soup.find_all("ul")]
if any("may refer to:" in p for p in page_content):
obs = search("[" + entity + "]")
else:
page = ""
for content in page_content:
if len(content.split(" ")) > 2:
page += decode_str(content)
if not content.endswith("\n"):
page += "\n"
obs = get_page_sentence(page, count=count)
return obs
@@ -0,0 +1,127 @@
import asyncio
import os
from dataclasses import dataclass, field
from typing import List
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
from wiki_search import search
from python_repl import python
load_dotenv()
_TRIGGERING_PROMPT = (
"Determine which next function to use, and respond using stringfield JSON object.\n"
"If you have completed all your tasks, make sure to use the 'finish' function to signal "
"and remember show your results."
)
def _search_tool(entity: str, count: int = 10) -> str:
"""Search Wikipedia for an entity and return the first sentences."""
return search(entity, count)
def _python_tool(command: str) -> str:
"""Execute a Python command and return the output."""
return python(command)
def _finish_tool(response: str) -> str:
"""Signal that all goals are completed and show results."""
return response
@dataclass
class AutoGPTInput:
name: str = "FilmTriviaGPT"
goals: List[str] = field(default_factory=lambda: [
"Introduce 'Lord of the Rings' film trilogy including the film title, "
"release year, director, current age of the director, production company "
"and a brief summary of the film."
])
role: str = (
"an AI specialized in film trivia that provides accurate and up-to-date "
"information about movies, directors, actors, and more."
)
class AutoGPTExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4"),
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="AutoGPTAgent",
instructions="", # will be set dynamically per run
tools=[_search_tool, _python_tool, _finish_tool],
)
@handler
async def process(self, gpt_input: AutoGPTInput, ctx: WorkflowContext[Never, str]) -> None:
# Build system prompt
system_prompt = (
f"You are {gpt_input.name}, {gpt_input.role}\n"
"Play to your strengths as an LLM and pursue simple strategies "
"with no legal complications to complete all goals.\n"
"Your decisions must always be made independently without seeking "
"user assistance.\n\n"
"Performance Evaluation:\n"
"1. Continuously review and analyze your actions to ensure you are "
"performing to the best of your abilities.\n"
"2. Constructively self-criticize your big-picture behavior constantly.\n"
"3. Reflect on past decisions and strategies to refine your approach.\n"
"4. Every command has a cost, so be smart and efficient. "
"Aim to complete tasks in the least number of steps.\n"
)
goals_text = "\n".join(f"{i + 1}. {g}" for i, g in enumerate(gpt_input.goals))
user_prompt = f"Goals:\n\n{goals_text}\n\n{_TRIGGERING_PROMPT}"
self._agent._instructions = system_prompt
response = await self._agent.run(user_prompt)
await ctx.yield_output(response.text)
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_executor = AutoGPTExecutor(id="autogpt")
return (
WorkflowBuilder(name="AutonomousAgentWorkflow", start_executor=_executor)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run(
AutoGPTInput(
name="FilmTriviaGPT",
goals=[
"Introduce 'Lord of the Rings' film trilogy including the film title, "
"release year, director, current age of the director, production company "
"and a brief summary of the film."
],
role=(
"an AI specialized in film trivia that provides accurate and up-to-date "
"information about movies, directors, actors, and more."
),
)
)
print(f"Output:\n{result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,66 @@
# Autonomous Agent
This is a flow showcasing how to construct a AutoGPT agent with promptflow to autonomously figures out how to apply the given functions
to solve the goal, which is film trivia that provides accurate and up-to-date information about movies, directors,
actors, and more in this sample.
It involves inferring next executed function and user intent with LLM, and then use the function to generate
observation. The observation above will be used as augmented prompt which is the input of next LLM inferring loop
until the inferred function is the signal that you have finished all your objectives. The functions set used in the
flow contains Wikipedia search function that can search the web to find answer about current events and PythonREPL
python function that can run python code in a REPL.
For the sample input about movie introduction, the AutoGPT usually runs 4 rounds to finish the task. The first round
is searching for the movie name, the second round is searching for the movie director, the third round is calculating
director age, and the last round is outputting finishing signal. It takes 30s~40s to finish the task, but may take
longer time if you use "gpt-3.5" or encounter Azure OpenAI rate limit. You could use "gpt-4" or go to
https://aka.ms/oai/quotaincrease if you would like to further increase the default rate limit.
Note: This is just a sample introducing how to use promptflow to build a simple AutoGPT. You can go to
https://github.com/Significant-Gravitas/Auto-GPT to get more concepts about AutoGPT.
## What you will learn
In this flow, you will learn
- how to use prompt tool.
- how to compose an AutoGPT flow using functions.
## Prerequisites
Install prompt-flow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Getting Started
### 1 Create Azure OpenAI or OpenAI connection
```bash
# Override keys with --set to avoid yaml file changes
pf connection create --file ../../../connections/azure_openai.yml --set api_key=<your_api_key> api_base=<your_api_base>
```
Note that you need to use "2023-07-01-preview" as Azure OpenAI connection API version when using function calling.
See <a href='https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling' target='_blank'>How to use function calling with Azure OpenAI Service</a> for more details.
### 2. Configure the flow with your connection
`flow.dag.yaml` is already configured with connection named `open_ai_connection`. It is recommended to use "gpt-4" model for stable performance. Using "gpt-3.5-turbo" may lead to the model getting stuck in the agent inner loop due to its suboptimal and unstable performance.
### 3. Test flow with single line data
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
```
### 4. Run with multi-line data
```bash
# create run using command line args
pf run create --flow . --data ./data.jsonl --column-mapping name='${data.name}' role='${data.role}' goals='${data.goals}' --stream
```
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
## Disclaimer
LLM systems are susceptible to prompt injection, and you can gain a deeper understanding of this issue in the [technical blog](https://developer.nvidia.com/blog/securing-llm-systems-against-prompt-injection/). As an illustration, the PythonREPL function might execute harmful code if provided with a malicious prompt within the provided sample. Furthermore, we cannot guarantee that implementing AST validations solely within the PythonREPL function will reliably elevate the sample's security to an enterprise level. We kindly remind you to refrain from utilizing this in a production environment.
@@ -0,0 +1,151 @@
from promptflow.tools.aoai import chat as aoai_chat
from promptflow.tools.openai import chat as openai_chat
from promptflow.connections import AzureOpenAIConnection, OpenAIConnection
from util import count_message_tokens, count_string_tokens, create_chat_message, generate_context, get_logger, \
parse_reply, construct_prompt
autogpt_logger = get_logger("autogpt_agent")
class AutoGPT:
def __init__(
self,
connection,
tools,
full_message_history,
functions,
tokens_per_message,
tokens_per_name,
system_prompt=None,
triggering_prompt=None,
user_prompt=None,
model_or_deployment_name=None
):
self.tools = tools
self.full_message_history = full_message_history
self.functions = functions
self.system_prompt = system_prompt
self.connection = connection
self.model_or_deployment_name = model_or_deployment_name
self.triggering_prompt = triggering_prompt
self.user_prompt = user_prompt
self.tokens_per_message = tokens_per_message
self.tokens_per_name = tokens_per_name
def chat_with_ai(self, token_limit):
"""Interact with the OpenAI API, sending the prompt, message history and functions."""
# Reserve 1000 tokens for the response
send_token_limit = token_limit - 1000
(
next_message_to_add_index,
current_tokens_used,
insertion_index,
current_context,
) = generate_context(self.system_prompt, self.full_message_history, self.user_prompt, self.tokens_per_message,
self.tokens_per_name)
# Account for user input (appended later)
current_tokens_used += count_message_tokens([create_chat_message("user", self.triggering_prompt)],
self.tokens_per_message, self.tokens_per_name)
current_tokens_used += 500 # Account for memory (appended later)
# Add Messages until the token limit is reached or there are no more messages to add.
while next_message_to_add_index >= 0:
message_to_add = self.full_message_history[next_message_to_add_index]
tokens_to_add = count_message_tokens([message_to_add], self.tokens_per_message,
self.tokens_per_name)
if current_tokens_used + tokens_to_add > send_token_limit:
break
# Add the most recent message to the start of the current context, after the two system prompts.
current_context.insert(
insertion_index, self.full_message_history[next_message_to_add_index]
)
# Count the currently used tokens
current_tokens_used += tokens_to_add
# Move to the next most recent message in the full message history
next_message_to_add_index -= 1
# Append user input, the length of this is accounted for above
current_context.extend([create_chat_message("user", self.triggering_prompt)])
# Calculate remaining tokens
tokens_remaining = token_limit - current_tokens_used
current_context = construct_prompt(current_context)
if isinstance(self.connection, AzureOpenAIConnection):
try:
response = aoai_chat(
connection=self.connection,
prompt=current_context,
deployment_name=self.model_or_deployment_name,
max_tokens=tokens_remaining,
functions=self.functions)
return response
except Exception as e:
if "The API deployment for this resource does not exist" in str(e):
raise Exception(
"Please fill in the deployment name of your Azure OpenAI resource gpt-4 model.")
elif isinstance(self.connection, OpenAIConnection):
response = openai_chat(
connection=self.connection,
prompt=current_context,
model=self.model_or_deployment_name,
max_tokens=tokens_remaining,
functions=self.functions)
return response
else:
raise ValueError("Connection must be an instance of AzureOpenAIConnection or OpenAIConnection")
def run(self):
tools = {t.__name__: t for t in self.tools}
while True:
# Send message to AI, get response
response = self.chat_with_ai(token_limit=4000)
if "function_call" in response:
# Update full message history
function_name = response["function_call"]["name"]
parsed_output = parse_reply(response["function_call"]["arguments"])
if "Error" in parsed_output:
error_message = parsed_output["Error"]
autogpt_logger.info(f"Error: {error_message}")
command_result = f"Error: {error_message}"
else:
autogpt_logger.info(f"Function generation requested, function = {function_name}, args = "
f"{parsed_output}")
self.full_message_history.append(
create_chat_message("assistant", f"Function generation requested, function = {function_name}, "
f"args = {parsed_output}")
)
if function_name == "finish":
response = parsed_output["response"]
autogpt_logger.info(f"Responding to user: {response}")
return response
if function_name in tools:
tool = tools[function_name]
try:
autogpt_logger.info(f"Next function = {function_name}, arguments = {parsed_output}")
result = tool(**parsed_output)
command_result = f"Executed function {function_name} and returned: {result}"
except Exception as e:
command_result = (
f"Error: {str(e)}, {type(e).__name__}"
)
result_length = count_string_tokens(command_result)
if result_length + 600 > 4000:
command_result = f"Failure: function {function_name} returned too much output. Do not " \
f"execute this function again with the same arguments."
else:
command_result = f"Unknown function '{function_name}'. Please refer to available functions " \
f"defined in functions parameter."
# Append command result to the message history
self.full_message_history.append(create_chat_message("function", str(command_result), function_name))
autogpt_logger.info(f"function: {command_result}")
else:
autogpt_logger.info(f"No function generated, returned: {response['content']}")
self.full_message_history.append(
create_chat_message("assistant", f"No function generated, returned: {response['content']}")
)
@@ -0,0 +1,33 @@
from typing import Union
from promptflow.core import tool
from promptflow.connections import AzureOpenAIConnection, OpenAIConnection
@tool
def autogpt_easy_start(connection: Union[AzureOpenAIConnection, OpenAIConnection], system_prompt: str, user_prompt: str,
triggering_prompt: str, functions: list, model_or_deployment_name: str, tokens_per_message: int,
tokens_per_name: int):
from wiki_search import search
from python_repl import python
from autogpt_class import AutoGPT
full_message_history = []
tools = [
search,
python
]
agent = AutoGPT(
full_message_history=full_message_history,
tools=tools,
system_prompt=system_prompt,
connection=connection,
model_or_deployment_name=model_or_deployment_name,
functions=functions,
user_prompt=user_prompt,
triggering_prompt=triggering_prompt,
tokens_per_message=tokens_per_message,
tokens_per_name=tokens_per_name,
)
result = agent.run()
return result
@@ -0,0 +1 @@
{"name": "FilmTriviaGPT", "role": "an AI specialized in film trivia that provides accurate and up-to-date information about movies, directors, actors, and more.", "goals": ["Introduce 'Lord of the Rings' film trilogy including the film title, release year, director, current age of the director, production company and a brief summary of the film."]}
@@ -0,0 +1,72 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
name:
type: string
default: "FilmTriviaGPT"
goals:
type: list
default: ["Introduce 'Lord of the Rings' film trilogy including the film title, release year, director, current age of the director, production company and a brief summary of the film."]
role:
type: string
default: "an AI specialized in film trivia that provides accurate and up-to-date information about movies, directors, actors, and more."
tokens_per_message:
type: int
default: 3
description: "This parameter is related with the model_or_deployment_name used in this flow, such as gpt-4 in this sample."
tokens_per_name:
type: int
default: 1
description: "This parameter is related with the model_or_deployment_name used in this flow, such as gpt-4 in this sample."
outputs:
output:
type: string
reference: ${autogpt_easy_start.output}
nodes:
- name: autogpt_easy_start
type: python
source:
type: code
path: autogpt_easy_start.py
inputs:
connection: open_ai_connection
functions: ${functions.output}
model_or_deployment_name: gpt-4
system_prompt: ${system_prompt.output}
triggering_prompt: ${triggering_prompt.output}
user_prompt: ${user_prompt.output}
tokens_per_message: ${inputs.tokens_per_message}
tokens_per_name: ${inputs.tokens_per_name}
- name: system_prompt
type: prompt
source:
type: code
path: system_prompt.jinja2
inputs:
name: ${inputs.name}
role: ${inputs.role}
- name: user_prompt
type: prompt
source:
type: code
path: user_prompt.jinja2
inputs:
goals: ${generate_goal.output}
- name: triggering_prompt
type: prompt
source:
type: code
path: triggering_prompt.jinja2
- name: functions
type: python
source:
type: code
path: functions.py
- name: generate_goal
type: python
source:
type: code
path: generate_goal.py
inputs:
items: ${inputs.goals}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,59 @@
from promptflow.core import tool
@tool
def functions_format() -> list:
functions = [
{
"name": "search",
"description": """The action will search this entity name on Wikipedia and returns the first {count}
sentences if it exists. If not, it will return some related entities to search next.""",
"parameters": {
"type": "object",
"properties": {
"entity": {
"type": "string",
"description": "Entity name which is used for Wikipedia search.",
},
"count": {
"type": "integer",
"default": 10,
"description": "Returned sentences count if entity name exists Wikipedia.",
},
},
"required": ["entity"],
},
},
{
"name": "python",
"description": """A Python shell. Use this to execute python commands. Input should be a valid python
command and you should print result with `print(...)` to see the output.""",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The command you want to execute in python",
}
},
"required": ["command"]
},
},
{
"name": "finish",
"description": """use this to signal that you have finished all your goals and remember show your
results""",
"parameters": {
"type": "object",
"properties": {
"response": {
"type": "string",
"description": "final response to let people know you have finished your goals and remember "
"show your results",
},
},
"required": ["response"],
},
},
]
return functions
@@ -0,0 +1,15 @@
from promptflow.core import tool
@tool
def generate_goal(items: list = []) -> str:
"""
Generate a numbered list from given items based on the item_type.
Args:
items (list): A list of items to be numbered.
Returns:
str: The formatted numbered list.
"""
return "\n".join(f"{i + 1}. {item}" for i, item in enumerate(items))
@@ -0,0 +1,127 @@
import sys
from io import StringIO
import functools
import logging
import ast
from typing import Dict, Optional
logger = logging.getLogger(__name__)
@functools.lru_cache(maxsize=None)
def warn_once() -> None:
# Warn that the PythonREPL
logger.warning("Python REPL can execute arbitrary code. Use with caution.")
COMMAND_EXECUTION_FUNCTIONS = ["system", "exec", "execfile", "eval"]
class PythonValidation:
def __init__(
self,
allow_imports: bool = False,
allow_command_exec: bool = False,
):
"""Initialize a PALValidation instance.
Args:
allow_imports (bool): Allow import statements.
allow_command_exec (bool): Allow using known command execution functions.
"""
self.allow_imports = allow_imports
self.allow_command_exec = allow_command_exec
def validate_code(self, code: str) -> None:
try:
code_tree = ast.parse(code)
except (SyntaxError, UnicodeDecodeError):
raise ValueError(f"Generated code is not valid python code: {code}")
except TypeError:
raise ValueError(
f"Generated code is expected to be a string, "
f"instead found {type(code)}"
)
except OverflowError:
raise ValueError(
f"Generated code too long / complex to be parsed by ast: {code}"
)
has_imports = False
top_level_nodes = list(ast.iter_child_nodes(code_tree))
for node in top_level_nodes:
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
has_imports = True
if not self.allow_imports and has_imports:
raise ValueError(f"Generated code has disallowed imports: {code}")
if (
not self.allow_command_exec
or not self.allow_imports
):
for node in ast.walk(code_tree):
if (
(not self.allow_command_exec)
and isinstance(node, ast.Call)
and (
(
hasattr(node.func, "id")
and node.func.id in COMMAND_EXECUTION_FUNCTIONS
)
or (
isinstance(node.func, ast.Attribute)
and node.func.attr in COMMAND_EXECUTION_FUNCTIONS
)
)
):
raise ValueError(
f"Found illegal command execution function "
f"{node.func.id} in code {code}"
)
if (not self.allow_imports) and (
isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom)
):
raise ValueError(f"Generated code has disallowed imports: {code}")
class PythonREPL:
"""Simulates a standalone Python REPL."""
def __init__(self) -> None:
self.globals: Optional[Dict] = globals()
self.locals: Optional[Dict] = None
self.code_validations = PythonValidation(allow_imports=True)
def run(self, command: str) -> str:
"""Run command with own globals/locals and returns anything printed."""
# Warn against dangers of PythonREPL
warn_once()
self.code_validations.validate_code(command)
old_stdout = sys.stdout
sys.stdout = my_stdout = StringIO()
try:
exec(command, self.globals, self.locals)
sys.stdout = old_stdout
output = my_stdout.getvalue()
except Exception as e:
sys.stdout = old_stdout
output = repr(e)
print(output)
return output
python_repl = PythonREPL()
def python(command: str):
"""
A Python shell. Use this to execute python commands. Input should be a valid python command.
If you want to see the output of a value, you should print it out with `print(...)`.
"""
command = command.strip().strip("```")
return python_repl.run(command)
@@ -0,0 +1,4 @@
promptflow
promptflow-tools
tiktoken
bs4
@@ -0,0 +1 @@
You are {{name}}, {{role}}
@@ -0,0 +1 @@
Determine which next function to use, and respond using stringfield JSON object.
@@ -0,0 +1 @@
Goals:
@@ -0,0 +1,166 @@
import time
from typing import List
import re
import tiktoken
import logging
import sys
import json
FORMATTER = logging.Formatter(
fmt="[%(asctime)s] %(name)-8s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S %z",
)
def get_logger(name: str, level=logging.INFO) -> logging.Logger:
logger = logging.Logger(name)
# log to sys.stdout for backward compatibility.
# TODO: May need to be removed in the future, after local/blob file stream are fully supported.
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(FORMATTER)
logger.addHandler(stdout_handler)
logger.setLevel(level)
return logger
def parse_reply(text: str):
try:
parsed = json.loads(text, strict=False)
except json.JSONDecodeError:
preprocessed_text = preprocess_json_input(text)
try:
parsed = json.loads(preprocessed_text, strict=False)
except Exception:
return {"Error": f"Could not parse invalid json: {text}"}
except TypeError:
return {"Error": f"the JSON object must be str, bytes or bytearray not {type(text)}"}
return parsed
def count_message_tokens(
messages: List, tokens_per_message: int, tokens_per_name: int, model: str = "gpt-3.5-turbo-0301"
) -> int:
"""
Returns the number of tokens used by a list of messages.
Args:
messages (list): A list of messages, each of which is a dictionary
containing the role and content of the message.
model (str): The name of the model to use for tokenization.
Defaults to "gpt-3.5-turbo-0301".
Returns:
int: The number of tokens used by the list of messages.
"""
tokens_per_message = tokens_per_message
tokens_per_name = tokens_per_name
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
if model == "gpt-3.5-turbo":
# !Note: gpt-3.5-turbo may change over time.
# Returning num tokens assuming gpt-3.5-turbo-0301.")
return count_message_tokens(messages, tokens_per_message, tokens_per_name, model="gpt-3.5-turbo-0301")
elif model == "gpt-4":
# !Note: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.")
return count_message_tokens(messages, tokens_per_message, tokens_per_name, model="gpt-4-0314")
elif model == "gpt-3.5-turbo-0301":
tokens_per_message = (
4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
)
tokens_per_name = -1 # if there's a name, the role is omitted
elif model == "gpt-4-0314":
tokens_per_message = 3
tokens_per_name = 1
else:
raise NotImplementedError(
f"num_tokens_from_messages() is not implemented for model {model}.\n"
" See https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken for"
" information on how messages are converted to tokens."
)
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
def count_string_tokens(string: str, model_name="gpt-3.5-turbo") -> int:
"""
Returns the number of tokens in a text string.
Args:
string (str): The text string.
model_name (str): The name of the encoding to use. (e.g., "gpt-3.5-turbo")
Returns:
int: The number of tokens in the text string.
"""
encoding = tiktoken.encoding_for_model(model_name)
return len(encoding.encode(string))
def create_chat_message(role, content, name=None):
"""
Create a chat message with the given role and content.
Args:
role (str): The role of the message sender, e.g., "system", "user", or "assistant".
content (str): The content of the message.
Returns:
dict: A dictionary containing the role and content of the message.
"""
if name is None:
return {"role": role, "content": content}
else:
return {"role": role, "name": name, "content": content}
def generate_context(prompt, full_message_history, user_prompt, tokens_per_message, tokens_per_name,
model="gpt-3.5-turbo"):
current_context = [
create_chat_message("system", prompt),
create_chat_message(
"system", f"The current time and date is {time.strftime('%c')}"
),
create_chat_message("user", user_prompt),
]
# Add messages from the full message history until we reach the token limit
next_message_to_add_index = len(full_message_history) - 1
insertion_index = len(current_context)
# Count the currently used tokens
current_tokens_used = count_message_tokens(current_context, tokens_per_message, tokens_per_name, model)
return (
next_message_to_add_index,
current_tokens_used,
insertion_index,
current_context,
)
def preprocess_json_input(input_str: str) -> str:
# Replace single backslashes with double backslashes, while leaving already escaped ones intact
corrected_str = re.sub(r'(?<!\\)\\(?!["\\/bfnrt]|u[0-9a-fA-F]{4})', r"\\\\", input_str)
return corrected_str
def construct_prompt(current_context):
update_current_context = []
for item in current_context:
role = item.get("role", None)
content = item.get("content", None)
name = item.get("name", None)
if name is not None:
update_current_context.append(":\n".join([role, "name", name]) + "\n" + ":\n".join(["content", content]))
else:
update_current_context.append(":\n".join([role, content]))
update_current_context = "\n".join(update_current_context)
return update_current_context
@@ -0,0 +1,62 @@
from bs4 import BeautifulSoup
import re
import requests
def decode_str(string):
return string.encode().decode("unicode-escape").encode("latin1").decode("utf-8")
def get_page_sentence(page, count: int = 10):
# find all paragraphs
paragraphs = page.split("\n")
paragraphs = [p.strip() for p in paragraphs if p.strip()]
# find all sentence
sentences = []
for p in paragraphs:
sentences += p.split('. ')
sentences = [s.strip() + '.' for s in sentences if s.strip()]
# get first `count` number of sentences
return ' '.join(sentences[:count])
def remove_nested_parentheses(string):
pattern = r'\([^()]+\)'
while re.search(pattern, string):
string = re.sub(pattern, '', string)
return string
def search(entity: str, count: int = 10):
"""
The input is an exact entity name. The action will search this entity name on Wikipedia and returns the first
count sentences if it exists. If not, it will return some related entities to search next.
"""
entity_ = entity.replace(" ", "+")
search_url = f"https://en.wikipedia.org/w/index.php?search={entity_}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35"
}
response_text = requests.get(search_url, headers=headers).text
soup = BeautifulSoup(response_text, features="html.parser")
result_divs = soup.find_all("div", {"class": "mw-search-result-heading"})
if result_divs: # mismatch
result_titles = [decode_str(div.get_text().strip()) for div in result_divs]
result_titles = [remove_nested_parentheses(result_title) for result_title in result_titles]
obs = f"Could not find {entity}. Similar: {result_titles[:5]}."
else:
page_content = [p_ul.get_text().strip() for p_ul in soup.find_all("p") + soup.find_all("ul")]
if any("may refer to:" in p for p in page_content):
obs = search("[" + entity + "]")
else:
page = ""
for content in page_content:
if len(content.split(" ")) > 2:
page += decode_str(content)
if not content.endswith("\n"):
page += "\n"
obs = get_page_sentence(page, count=count)
return obs
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-35-turbo-instruct
@@ -0,0 +1,3 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
@@ -0,0 +1,14 @@
import asyncio
from workflow import create_workflow
async def main():
workflow = create_workflow()
result = await workflow.run("Hello World!")
output = result.get_outputs()[0]
print(f"Output: {output}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,64 @@
import asyncio
import os
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
load_dotenv()
_PROMPT_TEMPLATE = "Write a simple {text} program that displays the greeting message."
class PromptExecutor(Executor):
@handler
async def receive(self, text: str, ctx: WorkflowContext[str]) -> None:
prompt = _PROMPT_TEMPLATE.format(text=text)
await ctx.send_message(prompt)
class LLMExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="BasicAgent",
instructions="You are a helpful assistant.",
)
@handler
async def call_llm(self, prompt: str, ctx: WorkflowContext[Never, str]) -> None:
response = await self._agent.run(prompt)
await ctx.yield_output(response.text)
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_prompt = PromptExecutor(id="hello_prompt")
_llm = LLMExecutor(id="llm")
return (
WorkflowBuilder(name="BasicWorkflow", start_executor=_prompt)
.add_edge(_prompt, _llm)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run("Hello World!")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-35-turbo
@@ -0,0 +1,3 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
@@ -0,0 +1,14 @@
import asyncio
from workflow import create_workflow
async def main():
workflow = create_workflow()
result = await workflow.run("Python Hello World!")
output = result.get_outputs()[0]
print(f"Output: {output}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,67 @@
import asyncio
import os
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
load_dotenv()
_SYSTEM_PROMPT = (
"You are a assistant which can write code. Response should only contain code."
)
_USER_TEMPLATE = "Write a simple {text} program that displays the greeting message."
class PromptExecutor(Executor):
@handler
async def receive(self, text: str, ctx: WorkflowContext[str]) -> None:
prompt = _USER_TEMPLATE.format(text=text)
await ctx.send_message(prompt)
class LLMExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="CodeAgent",
instructions=_SYSTEM_PROMPT,
)
@handler
async def call_llm(self, prompt: str, ctx: WorkflowContext[Never, str]) -> None:
response = await self._agent.run(prompt)
await ctx.yield_output(response.text)
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_prompt = PromptExecutor(id="hello_prompt")
_llm = LLMExecutor(id="llm")
return (
WorkflowBuilder(name="BasicBuiltinLLMWorkflow", start_executor=_prompt)
.add_edge(_prompt, _llm)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run("Python Hello World!")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,74 @@
# Basic flow with builtin llm tool
A basic standard flow that calls Azure OpenAI with builtin llm tool.
Tools used in this flow
- `prompt` tool
- built-in `llm` tool
Connections used in this flow:
- `azure_open_ai` connection
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Setup connection
Prepare your Azure OpenAI resource follow this [instruction](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) and get your `api_key` if you don't have one.
Note in this example, we are using [chat api](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt?pivots=programming-language-chat-completions), please use `gpt-35-turbo` or `gpt-4` model deployment.
Ensure you have created `open_ai_connection` connection before.
```bash
pf connection show -n open_ai_connection
```
Create connection if you haven't done that. Ensure you have put your azure OpenAI endpoint key in [azure_openai.yml](../../../connections/azure_openai.yml) file.
```bash
# Override keys with --set to avoid yaml file changes
pf connection create -f ../../../connections/azure_openai.yml --name open_ai_connection --set api_key=<your_api_key> api_base=<your_api_base>
```
## Run flow
### Run with single line input
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
# test with inputs
pf flow test --flow . --inputs text="Python Hello World!"
```
### run with multiple lines data
- create run
```bash
pf run create --flow . --data ./data.jsonl --column-mapping text='${data.text}' --stream
```
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
- list and show run meta
```bash
# list created run
pf run list
# get a sample run name
name=$(pf run list -r 10 | jq '.[] | select(.name | contains("basic_with_builtin_llm")) | .name'| head -n 1 | tr -d '"')
# show specific run detail
pf run show --name $name
# show output
pf run show-details --name $name
# visualize run in browser
pf run visualize --name $name
```
@@ -0,0 +1,3 @@
{"text": "Python Hello World!"}
{"text": "C Hello World!"}
{"text": "C# Hello World!"}
@@ -0,0 +1,34 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
text:
type: string
default: Python Hello World!
outputs:
output:
type: string
reference: ${llm.output}
nodes:
- name: hello_prompt
type: prompt
inputs:
text: ${inputs.text}
source:
type: code
path: hello.jinja2
- name: llm
type: llm
inputs:
prompt: ${hello_prompt.output}
# This is to easily switch between openai and azure openai.
# deployment_name is required by azure openai, model is required by openai.
deployment_name: gpt-35-turbo
model: gpt-3.5-turbo
max_tokens: '120'
source:
type: code
path: hello.jinja2
connection: open_ai_connection
api: chat
node_variants: {}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,5 @@
# system:
You are a assistant which can write code. Response should only contain code.
# user:
Write a simple {{text}} program that displays the greeting message.
@@ -0,0 +1,3 @@
promptflow
promptflow-tools
python-dotenv
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-35-turbo-instruct
@@ -0,0 +1,3 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
@@ -0,0 +1,14 @@
import asyncio
from workflow import create_workflow
async def main():
workflow = create_workflow()
result = await workflow.run("Hello World!")
output = result.get_outputs()[0]
print(f"Output: {output}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,64 @@
import asyncio
import os
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
load_dotenv()
_PROMPT_TEMPLATE = "Write a simple {text} program that displays the greeting message."
class PromptExecutor(Executor):
@handler
async def receive(self, text: str, ctx: WorkflowContext[str]) -> None:
prompt = _PROMPT_TEMPLATE.format(text=text)
await ctx.send_message(prompt)
class LLMExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="BasicConnectionAgent",
instructions="You are a helpful assistant.",
)
@handler
async def call_llm(self, prompt: str, ctx: WorkflowContext[Never, str]) -> None:
response = await self._agent.run(prompt)
await ctx.yield_output(response.text)
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_prompt = PromptExecutor(id="hello_prompt")
_llm = LLMExecutor(id="llm")
return (
WorkflowBuilder(name="BasicWithConnectionWorkflow", start_executor=_prompt)
.add_edge(_prompt, _llm)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run("Hello World!")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,108 @@
# Basic flow with custom connection
A basic standard flow that using custom python tool calls Azure OpenAI with connection info stored in custom connection.
Tools used in this flow
- `prompt` tool
- custom `python` Tool
Connections used in this flow:
- None
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Setup connection
Prepare your Azure OpenAI resource follow this [instruction](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) and get your `api_key` if you don't have one.
Create connection if you haven't done that.
```bash
# Override keys with --set to avoid yaml file changes
pf connection create -f custom.yml --set secrets.api_key=<your_api_key> configs.api_base=<your_api_base>
```
Ensure you have created `basic_custom_connection` connection.
```bash
pf connection show -n basic_custom_connection
```
## Run flow
### Run with single line input
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
# test with flow inputs
pf flow test --flow . --inputs text="Hello World!"
# test node with inputs
pf flow test --flow . --node llm --inputs prompt="Write a simple Hello World! program that displays the greeting message."
```
### Run with multiple lines data
- create run
```bash
pf run create --flow . --data ./data.jsonl --column-mapping text='${data.text}' --stream
```
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
- list and show run meta
```bash
# list created run
pf run list -r 3
# get a sample run name
name=$(pf run list -r 10 | jq '.[] | select(.name | contains("basic_with_connection")) | .name'| head -n 1 | tr -d '"')
# show specific run detail
pf run show --name $name
# show output
pf run show-details --name $name
# visualize run in browser
pf run visualize --name $name
```
### Run with connection override
Ensure you have created `open_ai_connection` connection before.
```bash
pf connection show -n open_ai_connection
```
Create connection if you haven't done that.
```bash
# Override keys with --set to avoid yaml file changes
pf connection create --file ../../../connections/azure_openai.yml --set api_key=<your_api_key> api_base=<your_api_base>
```
Run flow with newly created connection.
```bash
pf run create --flow . --data ./data.jsonl --connections llm.connection=open_ai_connection --column-mapping text='${data.text}' --stream
```
### Run in cloud with connection override
Ensure you have created `open_ai_connection` connection in cloud. Reference [this notebook](../../../tutorials/get-started/quickstart-azure.ipynb) on how to create connections in cloud with UI.
Run flow with connection `open_ai_connection`.
```bash
# set default workspace
az account set -s <your_subscription_id>
az configure --defaults group=<your_resource_group_name> workspace=<your_workspace_name>
pfazure run create --flow . --data ./data.jsonl --connections llm.connection=open_ai_connection --column-mapping text='${data.text}' --stream
```
@@ -0,0 +1,9 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/CustomConnection.schema.json
name: basic_custom_connection
type: custom
configs:
api_type: azure
api_version: 2023-03-15-preview
api_base: https://<to-be-replaced>.openai.azure.com/
secrets: # must-have
api_key: <to-be-replaced>
@@ -0,0 +1,3 @@
{"text": "Python Hello World!"}
{"text": "C Hello World!"}
{"text": "C# Hello World!"}
@@ -0,0 +1,29 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
text:
type: string
default: Hello World!
outputs:
output:
type: string
reference: ${llm.output}
nodes:
- name: hello_prompt
type: prompt
source:
type: code
path: hello.jinja2
inputs:
text: ${inputs.text}
- name: llm
type: python
source:
type: code
path: hello.py
inputs:
connection: basic_custom_connection
deployment_name: gpt-35-turbo-instruct
max_tokens: "120"
prompt: ${hello_prompt.output}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,2 @@
{# Please replace the template with your own prompt. #}
Write a simple {{text}} program that displays the greeting message.
@@ -0,0 +1,85 @@
from typing import Union
from openai.version import VERSION as OPENAI_VERSION
from promptflow.core import tool
from promptflow.connections import CustomConnection, AzureOpenAIConnection
# The inputs section will change based on the arguments of the tool function, after you save the code
# Adding type to arguments and return value will help the system show the types properly
# Please update the function name/signature per need
def to_bool(value) -> bool:
return str(value).lower() == "true"
def get_client(connection: Union[CustomConnection, AzureOpenAIConnection]):
if OPENAI_VERSION.startswith("0."):
raise Exception(
"Please upgrade your OpenAI package to version >= 1.0.0 or using the command: pip install --upgrade openai."
)
# connection can be extract as a dict object contains the configs and secrets
connection_dict = dict(connection)
api_key = connection_dict.get("api_key")
conn = dict(
api_key=api_key,
)
if api_key.startswith("sk-"):
from openai import OpenAI as Client
else:
from openai import AzureOpenAI as Client
conn.update(
azure_endpoint=connection_dict.get("api_base"),
api_version=connection_dict.get("api_version", "2023-07-01-preview"),
)
return Client(**conn)
@tool
def my_python_tool(
prompt: str,
# for AOAI, deployment name is customized by user, not model name.
deployment_name: str,
suffix: str = None,
max_tokens: int = 120,
temperature: float = 1.0,
top_p: float = 1.0,
n: int = 1,
logprobs: int = None,
echo: bool = False,
stop: list = None,
presence_penalty: float = 0,
frequency_penalty: float = 0,
best_of: int = 1,
logit_bias: dict = {},
user: str = "",
connection: Union[CustomConnection, AzureOpenAIConnection] = None,
**kwargs,
) -> str:
# TODO: remove below type conversion after client can pass json rather than string.
echo = to_bool(echo)
response = get_client(connection).completions.create(
prompt=prompt,
model=deployment_name,
# empty string suffix should be treated as None.
suffix=suffix if suffix else None,
max_tokens=int(max_tokens),
temperature=float(temperature),
top_p=float(top_p),
n=int(n),
logprobs=int(logprobs) if logprobs else None,
echo=echo,
# fix bug "[] is not valid under any of the given schemas-'stop'"
stop=stop if stop else None,
presence_penalty=float(presence_penalty),
frequency_penalty=float(frequency_penalty),
best_of=int(best_of),
# Logit bias must be a dict if we passed it to openai api.
logit_bias=logit_bias if logit_bias else {},
user=user,
)
# get first element because prompt is single.
return response.choices[0].text
@@ -0,0 +1,3 @@
promptflow[azure]
promptflow-tools
python-dotenv
@@ -0,0 +1,3 @@
AZURE_OPENAI_API_KEY=<your_AOAI_key>
AZURE_OPENAI_API_BASE=<your_AOAI_endpoint>
AZURE_OPENAI_API_TYPE=azure
+134
View File
@@ -0,0 +1,134 @@
# Basic standard flow
A basic standard flow using custom python tool that calls Azure OpenAI with connection info stored in environment variables.
Tools used in this flow
- `prompt` tool
- custom `python` Tool
Connections used in this flow:
- None
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Run flow
- Prepare your Azure OpenAI resource follow this [instruction](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) and get your `api_key` if you don't have one.
- Setup environment variables
Ensure you have put your azure OpenAI endpoint key in [.env](.env) file. You can create one refer to this [example file](.env.example).
```bash
cat .env
```
- Test flow/node
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
# test with flow inputs
pf flow test --flow . --inputs text="Java Hello World!"
# test node with inputs
pf flow test --flow . --node llm --inputs prompt="Write a simple Hello World program that displays the greeting message."
```
- Create run with multiple lines data
```bash
# using environment from .env file (loaded in user code: hello.py)
pf run create --flow . --data ./data.jsonl --column-mapping text='${data.text}' --stream
```
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
- List and show run meta
```bash
# list created run
pf run list
# get a sample run name
name=$(pf run list -r 10 | jq '.[] | select(.name | contains("basic_variant_0")) | .name'| head -n 1 | tr -d '"')
# show specific run detail
pf run show --name $name
# show output
pf run show-details --name $name
# visualize run in browser
pf run visualize --name $name
```
## Run flow with connection
Storing connection info in .env with plaintext is not safe. We recommend to use `pf connection` to guard secrets like `api_key` from leak.
- Show or create `open_ai_connection`
```bash
# create connection from `azure_openai.yml` file
# Override keys with --set to avoid yaml file changes
pf connection create --file ../../../connections/azure_openai.yml --set api_key=<your_api_key> api_base=<your_api_base>
# check if connection exists
pf connection show -n open_ai_connection
```
- Test using connection secret specified in environment variables
**Note**: we used `'` to wrap value since it supports raw value without escape in powershell & bash. For windows command prompt, you may remove the `'` to avoid it become part of the value.
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow . --environment-variables AZURE_OPENAI_API_KEY='${open_ai_connection.api_key}' AZURE_OPENAI_API_BASE='${open_ai_connection.api_base}'
```
- Create run using connection secret binding specified in environment variables, see [run.yml](run.yml)
```bash
# create run
pf run create --flow . --data ./data.jsonl --stream --environment-variables AZURE_OPENAI_API_KEY='${open_ai_connection.api_key}' AZURE_OPENAI_API_BASE='${open_ai_connection.api_base}' --column-mapping text='${data.text}'
# create run using yaml file
pf run create --file run.yml --stream
# show outputs
name=$(pf run list -r 10 | jq '.[] | select(.name | contains("basic_variant_0")) | .name'| head -n 1 | tr -d '"')
pf run show-details --name $name
```
## Run flow in cloud with connection
- Assume we already have a connection named `open_ai_connection` in workspace.
```bash
# set default workspace
az account set -s <your_subscription_id>
az configure --defaults group=<your_resource_group_name> workspace=<your_workspace_name>
```
- Create run
```bash
# run with environment variable reference connection in azureml workspace
pfazure run create --flow . --data ./data.jsonl --environment-variables AZURE_OPENAI_API_KEY='${open_ai_connection.api_key}' AZURE_OPENAI_API_BASE='${open_ai_connection.api_base}' --column-mapping text='${data.text}' --stream
# run using yaml file
pfazure run create --file run.yml --stream
```
- List and show run meta
```bash
# list created run
pfazure run list -r 3
# get a sample run name
name=$(pfazure run list -r 100 | jq '.[] | select(.name | contains("basic_variant_0")) | .name'| head -n 1 | tr -d '"')
# show specific run detail
pfazure run show --name $name
# show output
pfazure run show-details --name $name
# visualize run in browser
pfazure run visualize --name $name
```
+3
View File
@@ -0,0 +1,3 @@
{"text": "Python Hello World!"}
{"text": "C Hello World!"}
{"text": "C# Hello World!"}
@@ -0,0 +1,28 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
environment:
python_requirements_txt: requirements.txt
inputs:
text:
type: string
default: Hello World!
outputs:
output:
type: string
reference: ${llm.output}
nodes:
- name: hello_prompt
type: prompt
source:
type: code
path: hello.jinja2
inputs:
text: ${inputs.text}
- name: llm
type: python
source:
type: code
path: hello.py
inputs:
prompt: ${hello_prompt.output}
deployment_name: gpt-35-turbo-instruct
max_tokens: "120"
@@ -0,0 +1,2 @@
{# Please replace the template with your own prompt. #}
Write a simple {{text}} program that displays the greeting message.
+88
View File
@@ -0,0 +1,88 @@
import os
from openai.version import VERSION as OPENAI_VERSION
from dotenv import load_dotenv
from promptflow.core import tool
# The inputs section will change based on the arguments of the tool function, after you save the code
# Adding type to arguments and return value will help the system show the types properly
# Please update the function name/signature per need
def to_bool(value) -> bool:
return str(value).lower() == "true"
def get_client():
if OPENAI_VERSION.startswith("0."):
raise Exception(
"Please upgrade your OpenAI package to version >= 1.0.0 or using the command: pip install --upgrade openai."
)
api_key = os.environ["AZURE_OPENAI_API_KEY"]
conn = dict(
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
if api_key.startswith("sk-"):
from openai import OpenAI as Client
else:
from openai import AzureOpenAI as Client
conn.update(
azure_endpoint=os.environ.get("AZURE_OPENAI_API_BASE", "azure"),
api_version=os.environ.get("OPENAI_API_VERSION", "2023-07-01-preview"),
)
return Client(**conn)
@tool
def my_python_tool(
prompt: str,
# for AOAI, deployment name is customized by user, not model name.
deployment_name: str,
suffix: str = None,
max_tokens: int = 120,
temperature: float = 1.0,
top_p: float = 1.0,
n: int = 1,
logprobs: int = None,
echo: bool = False,
stop: list = None,
presence_penalty: float = 0,
frequency_penalty: float = 0,
best_of: int = 1,
logit_bias: dict = {},
user: str = "",
**kwargs,
) -> str:
if "AZURE_OPENAI_API_KEY" not in os.environ or "AZURE_OPENAI_API_BASE" not in os.environ:
# load environment variables from .env file
load_dotenv()
if "AZURE_OPENAI_API_KEY" not in os.environ:
raise Exception("Please specify environment variables: AZURE_OPENAI_API_KEY")
# TODO: remove below type conversion after client can pass json rather than string.
echo = to_bool(echo)
response = get_client().completions.create(
prompt=prompt,
model=deployment_name,
# empty string suffix should be treated as None.
suffix=suffix if suffix else None,
max_tokens=int(max_tokens),
temperature=float(temperature),
top_p=float(top_p),
n=int(n),
logprobs=int(logprobs) if logprobs else None,
echo=echo,
# fix bug "[] is not valid under any of the given schemas-'stop'"
stop=stop if stop else None,
presence_penalty=float(presence_penalty),
frequency_penalty=float(frequency_penalty),
best_of=int(best_of),
# Logit bias must be a dict if we passed it to openai api.
logit_bias=logit_bias if logit_bias else {},
user=user,
)
# get first element because prompt is single.
return response.choices[0].text
@@ -0,0 +1,3 @@
promptflow[azure]
promptflow-tools
python-dotenv
+10
View File
@@ -0,0 +1,10 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Run.schema.json
flow: .
data: data.jsonl
environment_variables:
# environment variables from connection
AZURE_OPENAI_API_KEY: ${open_ai_connection.api_key}
AZURE_OPENAI_API_BASE: ${open_ai_connection.api_base}
AZURE_OPENAI_API_TYPE: azure
column_mapping:
text: ${data.text}
@@ -0,0 +1 @@
agent-framework>=1.0.1
@@ -0,0 +1,50 @@
import asyncio
from workflow import create_workflow
async def main():
"""Run the workflow with multiple inputs to demonstrate random conditions."""
test_questions = [
"What is Prompt flow?",
"How does it work?",
"Tell me more about the features",
"What is machine learning?",
]
print("=" * 70)
print("Testing Conditional Workflow with Random Conditions")
print("=" * 70)
print()
safe_count = 0
unsafe_count = 0
for i, question in enumerate(test_questions, 1):
workflow = create_workflow()
result = await workflow.run(question)
output = result.get_outputs()[0]
# Determine which path was taken based on the output
is_safe_path = "Prompt flow is a suite" in output
if is_safe_path:
safe_count += 1
status = "✓ SAFE"
else:
unsafe_count += 1
status = "✗ UNSAFE"
print(f"Test {i:2d} [{status}]: {question}")
print(f"{output}")
print()
print("=" * 70)
print("Results Summary:")
print(f" Safe paths: {safe_count}/{len(test_questions)}")
print(f" Unsafe paths: {unsafe_count}/{len(test_questions)}")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,65 @@
import asyncio
import random
from dataclasses import dataclass
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
@dataclass
class SafetyResult:
question: str
is_safe: bool
class ContentSafetyExecutor(Executor):
@handler
async def check(self, question: str, ctx: WorkflowContext[SafetyResult]) -> None:
# Placeholder: replace with a real content safety check.
is_safe = random.choice([True, False])
await ctx.send_message(SafetyResult(question=question, is_safe=is_safe))
class LLMResultExecutor(Executor):
@handler
async def run_llm(self, msg: SafetyResult, ctx: WorkflowContext[Never, str]) -> None:
# Placeholder: replace with a real LLM call.
answer = (
"Prompt flow is a suite of development tools designed to streamline "
"the end-to-end development cycle of LLM-based AI applications."
)
await ctx.yield_output(answer)
class DefaultResultExecutor(Executor):
@handler
async def default(self, msg: SafetyResult, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(f"I'm not familiar with your query: {msg.question}.")
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_safety = ContentSafetyExecutor(id="content_safety_check")
_llm = LLMResultExecutor(id="llm_result")
_default = DefaultResultExecutor(id="default_result")
return (
WorkflowBuilder(name="ConditionalIfElseWorkflow", start_executor=_safety)
.add_edge(_safety, _llm, condition=lambda msg: msg.is_safe)
.add_edge(_safety, _default, condition=lambda msg: not msg.is_safe)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run("What is Prompt flow?")
print(f"Answer: {result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,61 @@
# Conditional flow for if-else scenario
This example is a conditional flow for if-else scenario.
By following this example, you will learn how to create a conditional flow using the `activate config`.
## Flow description
In this flow, it checks if an input query passes content safety check. If it's denied, we'll return a default response; otherwise, we'll call LLM to get a response and then summarize the final results.
The following are two execution situations of this flow:
- if input query passes content safety check:
![content_safety_check_passed](content_safety_check_passed.png)
- else:
![content_safety_check_failed](content_safety_check_failed.png)
**Notice**: The `content_safety_check` and `llm_result` node in this flow are dummy nodes that do not actually use the conten safety tool and LLM tool. You can replace them with the real ones. Learn more: [LLM Tool](https://microsoft.github.io/promptflow/reference/tools-reference/llm-tool.html)
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Run flow
- Test flow
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
# test with flow inputs
pf flow test --flow . --inputs question="What is Prompt flow?"
```
- Create run with multiple lines of data
```bash
# create a random run name
run_name="conditional_flow_for_if_else_"$(openssl rand -hex 12)
# create run
pf run create --flow . --data ./data.jsonl --column-mapping question='${data.question}' --stream --name $run_name
```
- List and show run metadata
```bash
# list created run
pf run list
# show specific run detail
pf run show --name $run_name
# show output
pf run show-details --name $run_name
# visualize run in browser
pf run visualize --name $run_name
```
@@ -0,0 +1,8 @@
from promptflow.core import tool
import random
@tool
def content_safety_check(text: str) -> str:
# You can use a content safety node to replace this tool.
return random.choice([True, False])
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -0,0 +1,2 @@
{"question": "What is Prompt flow?"}
{"question": "What is ChatGPT?"}
@@ -0,0 +1,6 @@
from promptflow.core import tool
@tool
def default_result(question: str) -> str:
return f"I'm not familiar with your query: {question}."
@@ -0,0 +1,47 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
question:
type: string
default: What is Prompt flow?
outputs:
answer:
type: string
reference: ${generate_result.output}
nodes:
- name: content_safety_check
type: python
source:
type: code
path: content_safety_check.py
inputs:
text: ${inputs.question}
- name: llm_result
type: python
source:
type: code
path: llm_result.py
inputs:
question: ${inputs.question}
activate:
when: ${content_safety_check.output}
is: true
- name: default_result
type: python
source:
type: code
path: default_result.py
inputs:
question: ${inputs.question}
activate:
when: ${content_safety_check.output}
is: false
- name: generate_result
type: python
source:
type: code
path: generate_result.py
inputs:
llm_result: ${llm_result.output}
default_result: ${default_result.output}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,9 @@
from promptflow.core import tool
@tool
def generate_result(llm_result="", default_result="") -> str:
if llm_result:
return llm_result
else:
return default_result
@@ -0,0 +1,10 @@
from promptflow.core import tool
@tool
def llm_result(question: str) -> str:
# You can use an LLM node to replace this tool.
return (
"Prompt flow is a suite of development tools designed to streamline "
"the end-to-end development cycle of LLM-based AI applications."
)
@@ -0,0 +1,2 @@
promptflow
promptflow-tools
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-35-turbo
@@ -0,0 +1,3 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
@@ -0,0 +1,69 @@
import asyncio
from workflow import create_workflow
async def main():
"""Run the workflow with multiple queries to demonstrate the switch logic."""
# Test cases targeting each switch condition
test_cases = [
("When will my order be shipped?", "order_search"),
("Where is my order?", "order_search"),
("Can you track my package?", "order_search"),
("Tell me about this product", "product_info"),
("What is the specification?", "product_info"),
("Who manufactures this?", "product_info"),
("Recommend a good product", "product_recommendation"),
("What products do you suggest?", "product_recommendation"),
("Give me product recommendations", "product_recommendation"),
("Something completely random query", "default/unknown"),
]
print("=" * 80)
print("Testing Conditional Switch Workflow")
print("=" * 80)
print()
results_by_intention = {}
for i, (query, expected_intention) in enumerate(test_cases, 1):
print(f"Test {i:2d}: {query}")
print(f" Expected intention: {expected_intention}")
workflow = create_workflow()
result = await workflow.run(query)
response = result.get_outputs()[0]
print(f" Response: {response}")
# Track results
if expected_intention not in results_by_intention:
results_by_intention[expected_intention] = []
results_by_intention[expected_intention].append(response)
print()
print("=" * 80)
print("Summary of Switch Paths:")
print("=" * 80)
intention_labels = {
"order_search": "📦 Order Search",
"product_info": "️ Product Info",
"product_recommendation": "⭐ Product Recommendation",
"default/unknown": "❓ Default/Unknown",
}
for intention, label in intention_labels.items():
if intention in results_by_intention:
count = len(results_by_intention[intention])
print(f"{label}: {count} matched")
for response in results_by_intention[intention]:
print(f"{response}")
else:
print(f"{label}: 0 matched")
print("=" * 80)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,108 @@
import asyncio
import os
from dataclasses import dataclass
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
load_dotenv()
_CLASSIFY_SYSTEM_PROMPT = """\
There is a search bar in the mall APP and users can enter any query in the search bar.
The user may want to search for orders, view product information, or seek recommended products.
Therefore, please classify user intentions into the following three types \
according to the query: product_recommendation, order_search, product_info
Please note that only the above three situations can be returned, and try not to include other return values."""
@dataclass
class ClassifiedQuery:
query: str
intention: str
class ClassifyExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="ClassifyAgent",
instructions=_CLASSIFY_SYSTEM_PROMPT,
)
@handler
async def classify(self, query: str, ctx: WorkflowContext[ClassifiedQuery]) -> None:
response = await self._agent.run(f"The user's query is {query}")
llm_result = response.text
intentions_list = ["order_search", "product_info", "product_recommendation"]
matches = [i for i in intentions_list if i in llm_result.lower()]
intention = matches[0] if matches else "unknown"
await ctx.send_message(ClassifiedQuery(query=query, intention=intention))
class OrderSearchExecutor(Executor):
@handler
async def search(self, msg: ClassifiedQuery, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output("Your order is being mailed, please wait patiently.")
class ProductInfoExecutor(Executor):
@handler
async def info(self, msg: ClassifiedQuery, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output("This product is produced by Microsoft.")
class ProductRecommendationExecutor(Executor):
@handler
async def recommend(self, msg: ClassifiedQuery, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(
"I recommend promptflow to you, which can solve your problem very well."
)
class DefaultExecutor(Executor):
@handler
async def default(self, msg: ClassifiedQuery, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output("Sorry, no results matching your search were found.")
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_classify = ClassifyExecutor(id="classify")
_order = OrderSearchExecutor(id="order_search")
_product = ProductInfoExecutor(id="product_info")
_recommend = ProductRecommendationExecutor(id="product_recommendation")
_default = DefaultExecutor(id="default")
return (
WorkflowBuilder(name="ConditionalSwitchWorkflow", start_executor=_classify)
.add_edge(_classify, _order, condition=lambda m: m.intention == "order_search")
.add_edge(_classify, _product, condition=lambda m: m.intention == "product_info")
.add_edge(_classify, _recommend, condition=lambda m: m.intention == "product_recommendation")
.add_edge(_classify, _default, condition=lambda m: m.intention == "unknown")
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run("When will my order be shipped?")
print(f"Response: {result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,78 @@
# Conditional flow for switch scenario
This example is a conditional flow for switch scenario.
By following this example, you will learn how to create a conditional flow using the `activate config`.
## Flow description
In this flow, we set the background to the search function of a certain mall, use `activate config` to implement switch logic and determine user intent based on the input queries to achieve dynamic processing and generate user-oriented output.
- The `classify_with_llm` node analyzes user intent based on input query and provides one of the following results: "product_recommendation," "order_search," or "product_info".
- The `class_check` node generates the correctly formatted user intent.
- The `product_recommendation`, `order_search`, and `product_info` nodes are configured with activate config and are only executed when the output from `class_check` meets the specified conditions.
- The `generate_response` node generates user-facing output.
For example, as the shown below, the input query is "When will my order be shipped" and the LLM node classifies the user intent as "order_search", resulting in both the `product_info` and `product_recommendation` nodes being bypassed and only the `order_search` node being executed, and then generating the outputs.
![conditional_flow_for_switch](conditional_flow_for_switch.png)
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
## Setup connection
Prepare your Azure OpenAI resource follow this [instruction](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) and get your `api_key` if you don't have one.
Note in this example, we are using [chat api](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt?pivots=programming-language-chat-completions), please use `gpt-35-turbo` or `gpt-4` model deployment.
Create connection if you haven't done that. Ensure you have put your azure OpenAI endpoint key in [azure_openai.yml](../../../connections/azure_openai.yml) file.
```bash
# Override keys with --set to avoid yaml file changes
pf connection create -f ../../../connections/azure_openai.yml --name open_ai_connection --set api_key=<your_api_key> api_base=<your_api_base>
```
Note in [flow.dag.yaml](flow.dag.yaml) we are using connection named `open_ai_connection`.
```bash
# show registered connection
pf connection show --name open_ai_connection
```
## Run flow
- Test flow
```bash
# test with default input value in flow.dag.yaml
pf flow test --flow .
# test with flow inputs
pf flow test --flow . --inputs query="When will my order be shipped?"
```
- Create run with multiple lines of data
```bash
# create a random run name
run_name="conditional_flow_for_switch_"$(openssl rand -hex 12)
# create run
pf run create --flow . --data ./data.jsonl --column-mapping query='${data.query}' --stream --name $run_name
```
- List and show run metadata
```bash
# list created run
pf run list
# show specific run detail
pf run show --name $run_name
# show output
pf run show-details --name $run_name
# visualize run in browser
pf run visualize --name $run_name
```
@@ -0,0 +1,8 @@
from promptflow.core import tool
@tool
def class_check(llm_result: str) -> str:
intentions_list = ["order_search", "product_info", "product_recommendation"]
matches = [intention for intention in intentions_list if intention in llm_result.lower()]
return matches[0] if matches else "unknown"
@@ -0,0 +1,11 @@
# system:
There is a search bar in the mall APP and users can enter any query in the search bar.
The user may want to search for orders, view product information, or seek recommended products.
Therefore, please classify user intentions into the following three types according to the query: product_recommendation, order_search, product_info
Please note that only the above three situations can be returned, and try not to include other return values.
# user:
The user's query is {{query}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,3 @@
{"query": "When will my order be shipped?"}
{"query": "Can you help me find information about this T-shirt?"}
{"query": "Can you recommend me a useful prompt tool?"}
@@ -0,0 +1,69 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
query:
type: string
default: When will my order be shipped?
outputs:
response:
type: string
reference: ${generate_response.output}
nodes:
- name: classify_with_llm
type: llm
source:
type: code
path: classify_with_llm.jinja2
inputs:
deployment_name: gpt-35-turbo
max_tokens: 128
query: ${inputs.query}
connection: open_ai_connection
api: chat
- name: class_check
type: python
source:
type: code
path: class_check.py
inputs:
llm_result: ${classify_with_llm.output}
- name: order_search
type: python
source:
type: code
path: order_search.py
inputs:
query: ${inputs.query}
activate:
when: ${class_check.output}
is: order_search
- name: product_info
type: python
source:
type: code
path: product_info.py
inputs:
query: ${inputs.query}
activate:
when: ${class_check.output}
is: product_info
- name: product_recommendation
type: python
source:
type: code
path: product_recommendation.py
inputs:
query: ${inputs.query}
activate:
when: ${class_check.output}
is: product_recommendation
- name: generate_response
type: python
source:
type: code
path: generate_response.py
inputs:
order_search: ${order_search.output}
product_info: ${product_info.output}
product_recommendation: ${product_recommendation.output}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,8 @@
from promptflow.core import tool
@tool
def generate_response(order_search="", product_info="", product_recommendation="") -> str:
default_response = "Sorry, no results matching your search were found."
responses = [order_search, product_info, product_recommendation]
return next((response for response in responses if response), default_response)
@@ -0,0 +1,7 @@
from promptflow.core import tool
@tool
def order_search(query: str) -> str:
print(f"Your query is {query}.\nSearching for order...")
return "Your order is being mailed, please wait patiently."
@@ -0,0 +1,7 @@
from promptflow.core import tool
@tool
def product_info(query: str) -> str:
print(f"Your query is {query}.\nLooking for product information...")
return "This product is produced by Microsoft."
@@ -0,0 +1,7 @@
from promptflow.core import tool
@tool
def product_recommendation(query: str) -> str:
print(f"Your query is {query}.\nRecommending products...")
return "I recommend promptflow to you, which can solve your problem very well."
@@ -0,0 +1,2 @@
promptflow
promptflow-tools
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-35-turbo
@@ -0,0 +1,3 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
@@ -0,0 +1,18 @@
import asyncio
from workflow import IntentInput, create_workflow
async def main():
workflow = create_workflow()
result = await workflow.run(
IntentInput(
history="Customer: I want to return my order\nAgent: Sure, I can help with that.",
customer_info="Name: John Doe\nOrder: #12345 - Widget A",
)
)
print(f"Intent: {result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,102 @@
import asyncio
import os
from dataclasses import dataclass
from dotenv import load_dotenv
from typing_extensions import Never
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework.openai import OpenAIChatClient
load_dotenv()
_INTENT_SYSTEM_PROMPT = """\
You are given a list of orders with item_numbers from a customer and a statement from the customer. \
It is your job to identify the intent that the customer has with their statement. \
Possible intents can be: "product return", "product exchange", "general question", "product question", "other"."""
_INTENT_USER_TEMPLATE = """\
In triple backticks below is the customer information and a list of orders.
```
{customer_info}
```
In triple backticks below are the is the chat history with customer \
statements and replies from the customer service agent:
```
{history}
```
What is the customer's `intent:` here?
"product return", "exchange product", "general question", "product question" or "other"?
Reply with only the intent string."""
@dataclass
class IntentInput:
history: str
customer_info: str
class PromptExecutor(Executor):
@handler
async def receive(self, intent_input: IntentInput, ctx: WorkflowContext[str]) -> None:
prompt = _INTENT_USER_TEMPLATE.format(
customer_info=intent_input.customer_info,
history=intent_input.history,
)
await ctx.send_message(prompt)
class ExtractIntentExecutor(Executor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
client = OpenAIChatClient(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
self._agent = Agent(
client=client,
name="IntentAgent",
instructions=_INTENT_SYSTEM_PROMPT,
)
@handler
async def extract(self, prompt: str, ctx: WorkflowContext[Never, str]) -> None:
response = await self._agent.run(prompt)
await ctx.yield_output(response.text)
def create_workflow():
"""Create a fresh workflow instance.
MAF workflows do not support concurrent execution, so each
concurrent caller needs its own workflow instance.
"""
_prompt = PromptExecutor(id="chat_prompt")
_extract = ExtractIntentExecutor(id="extract_intent")
return (
WorkflowBuilder(name="CustomerIntentWorkflow", start_executor=_prompt)
.add_edge(_prompt, _extract)
.build()
)
async def main():
workflow = create_workflow()
result = await workflow.run(
IntentInput(
history="Customer: I want to return my order\nAgent: Sure, I can help with that.",
customer_info="Name: John Doe\nOrder: #12345 - Widget A",
)
)
print(f"Intent: {result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,7 @@
*.ipynb
.venv/
.data/
.env
.vscode/
outputs/
connection.json
@@ -0,0 +1,3 @@
CHAT_DEPLOYMENT_NAME=gpt-35-turbo
AZURE_OPENAI_API_KEY=<your_AOAI_key>
AZURE_OPENAI_API_BASE=<your_AOAI_endpoint>
@@ -0,0 +1,100 @@
# Customer Intent Extraction
This sample is using OpenAI chat model(ChatGPT/GPT4) to identify customer intent from customer's question.
By going through this sample you will learn how to create a flow from existing working code (written in LangChain in this case).
This is the [existing code](./intent.py).
## Prerequisites
Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
Ensure you have put your azure OpenAI endpoint key in .env file.
```bash
cat .env
```
## Run flow
1. init flow directory - create promptflow folder from existing python file
```bash
pf flow init --flow . --entry intent.py --function extract_intent --prompt-template chat_prompt=user_intent_zero_shot.jinja2
```
The generated files:
- extract_intent_tool.py: Wrap the func `extract_intent` in the `intent.py` script into a [Python Tool](https://promptflow.azurewebsites.net/tools-reference/python-tool.html).
- flow.dag.yaml: Describes the DAG(Directed Acyclic Graph) of this flow.
- .gitignore: File/folder in the flow to be ignored.
2. create needed custom connection
```bash
pf connection create -f .env --name custom_connection
```
3. test flow with single line input
```bash
pf flow test --flow . --inputs ./data/sample.json
```
4. run with multiple lines input
```bash
pf run create --flow . --data ./data --column-mapping history='${data.history}' customer_info='${data.customer_info}'
```
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
5. list/show
```bash
# list created run
pf run list
# get a sample completed run name
name=$(pf run list | jq '.[] | select(.name | contains("customer_intent_extraction")) | .name'| head -n 1 | tr -d '"')
# show run
pf run show --name $name
# show specific run detail, top 3 lines
pf run show-details --name $name -r 3
```
6. evaluation
```bash
# create evaluation run
pf run create --flow ../../evaluation/eval-classification-accuracy --data ./data --column-mapping groundtruth='${data.intent}' prediction='${run.outputs.output}' --run $name
```
```bash
# get the evaluation run in previous step
eval_run_name=$(pf run list | jq '.[] | select(.name | contains("eval_classification_accuracy")) | .name'| head -n 1 | tr -d '"')
# show run
pf run show --name $eval_run_name
# show run output
pf run show-details --name $eval_run_name -r 3
```
6. visualize
```bash
# visualize in browser
pf run visualize --name $eval_run_name # your evaluation run name
```
## Deploy
### Serve as a local test app
```bash
pf flow serve --source . --port 5123 --host localhost
```
Visit http://localhost:5213 to access the test app.
### Export
#### Export as docker
```bash
# pf flow export --source . --format docker --output ./package
```
@@ -0,0 +1,39 @@
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently bought the TrailMaster X4 Tent, and it leaked during a light rain. This is unacceptable! I expected a reliable and waterproof tent, but it failed to deliver. I'm extremely disappointed in the quality."}], "item_number": 1, "order_number": 2, "description": "TrailMaster X4 Tent, quantity 1, price $250", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: John \nLast Name: Smith \nAge: 35 \nEmail Address: johnsmith@example.com \nPhone Number: 555-123-4567 \nShipping Address: 123 Main St, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 1 \ndate: 2023-01-05 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 19 \ndate: 2023-01-25 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 29 \ndate: 2023-02-10 \nitem:\n- description: Alpine Explorer Tent, quantity 2, price $700 \n\u00a0 item_number: 8 \n\norder_number: 41 \ndate: 2023-03-01 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 50 \ndate: 2023-03-16 \nitem:\n- description: SkyView 2-Person Tent, quantity 2, price $400 \n\u00a0 item_number: 15 \n\norder_number: 59 \ndate: 2023-04-01 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I recently purchased two TrailMaster X4 Tents, and while the overall quality is good, I noticed a minor issue. One of the tent poles arrived slightly bent, making it challenging to assemble the tent properly. I'm concerned that this may affect the stability of the tent. Can you provide any guidance on how to address this?"}], "item_number": 1, "order_number": 1, "description": "TrailMaster X4 Tent, quantity 2, price $500", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jason \nLast Name: Brown \nAge: 50 \nEmail Address: jasonbrown@example.com \nPhone Number: 555-222-3333 \nShipping Address: 456 Cedar Rd, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 8 \ndate: 2023-03-20 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 27 \ndate: 2023-03-10 \nitem:\n- description: CozyNights Sleeping Bag, quantity 2, price $200 \n\u00a0 item_number: 7 \n\norder_number: 36 \ndate: 2023-03-25 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 2, price $150 \n\u00a0 item_number: 10 \n\norder_number: 43 \ndate: 2023-05-11 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 52 \ndate: 2023-05-26 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 57 \ndate: 2023-05-01 \nitem:\n- description: RainGuard Hiking Jacket, quantity 2, price $220 \n\u00a0 item_number: 17 \n\norder_number: 66 \ndate: 2023-05-16 \nitem:\n- description: CompactCook Camping Stove, quantity 2, price $120 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently purchased the Adventurer Pro Backpack, and I'm excited to use it for my upcoming camping trip. Can you provide some guidance on the backpack's capacity and any special features I should be aware of? I want to make sure I utilize it to its fullest potential."}], "item_number": 2, "order_number": 8, "description": "Adventurer Pro Backpack, quantity 3, price $270", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Daniel \nLast Name: Wilson \nAge: 47 \nEmail Address: danielw@example.com \nPhone Number: 555-444-5555 \nShipping Address: 321 Birch Ln, Smallville USA, 34567 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 9 \ndate: 2023-04-25 \nitem:\n- description: Adventurer Pro Backpack, quantity 3, price $270 \n\u00a0 item_number: 2 \n\norder_number: 13 \ndate: 2023-03-25 \nitem:\n- description: Summit Breeze Jacket, quantity 1, price $120 \n\u00a0 item_number: 3 \n\norder_number: 22 \ndate: 2023-05-07 \nitem:\n- description: BaseCamp Folding Table, quantity 3, price $180 \n\u00a0 item_number: 5 \n\norder_number: 40 \ndate: 2023-04-05 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 1, price $110 \n\u00a0 item_number: 11 \n\norder_number: 49 \ndate: 2023-05-21 \nitem:\n- description: MountainDream Sleeping Bag, quantity 1, price $130 \n\u00a0 item_number: 14 \n\n", "history": [{"role": "customer", "content": "I recently received the Adventurer Pro Backpack I ordered, but there seems to be a problem. One of the zippers on the main compartment is jammed, making it difficult to open and close. This is quite frustrating, as I was looking forward to using it on my upcoming hiking trip."}], "item_number": 2, "order_number": 9, "description": "Adventurer Pro Backpack, quantity 2, price $180", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Robert \nLast Name: Johnson \nAge: 36 \nEmail Address: robertj@example.com \nPhone Number: 555-555-1212 \nShipping Address: 123 Main St, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 10 \ndate: 2023-05-05 \nitem:\n- description: Adventurer Pro Backpack, quantity 2, price $180 \n\u00a0 item_number: 2 \n\n", "history": [{"role": "customer", "content": "I recently purchased two Adventurer Pro Backpacks, and I'm curious to know if they are waterproof. I'm planning to go on a camping trip where we might encounter some rain, and I want to make sure my belongings stay dry."}], "item_number": 2, "order_number": 10, "description": "Adventurer Pro Backpack, quantity 2, price $180", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Michael \nLast Name: Johnson \nAge: 45 \nEmail Address: michaelj@example.com \nPhone Number: 555-555-1212 \nShipping Address: 789 Elm St, Smallville USA, 34567 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 11 \ndate: 2023-01-15 \nitem:\n- description: Summit Breeze Jacket, quantity 1, price $120 \n\u00a0 item_number: 3 \n\norder_number: 20 \ndate: 2023-02-28 \nitem:\n- description: BaseCamp Folding Table, quantity 2, price $120 \n\u00a0 item_number: 5 \n\norder_number: 30 \ndate: 2023-03-15 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 38 \ndate: 2023-02-25 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 1, price $110 \n\u00a0 item_number: 11 \n\norder_number: 47 \ndate: 2023-03-11 \nitem:\n- description: MountainDream Sleeping Bag, quantity 1, price $130 \n\u00a0 item_number: 14 \n\norder_number: 60 \ndate: 2023-05-06 \nitem:\n- description: TrekStar Hiking Sandals, quantity 2, price $140 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I recently purchased the Summit Breeze Jacket, and I'm extremely disappointed. The jacket doesn't provide the protection it claims. I wore it during a light rain, and it soaked through within minutes. This is completely unacceptable!"}], "item_number": 3, "order_number": 11, "description": "Summit Breeze Jacket, quantity 1, price $120", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Melissa \nLast Name: Davis \nAge: 31 \nEmail Address: melissad@example.com \nPhone Number: 555-333-4444 \nShipping Address: 789 Ash St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 4 \ndate: 2023-04-22 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 17 \ndate: 2023-03-30 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 25 \ndate: 2023-04-10 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 34 \ndate: 2023-04-25 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 46 \ndate: 2023-05-16 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 55 \ndate: 2023-05-31 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 64 \ndate: 2023-06-16 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I recently purchased the TrekReady Hiking Boots, and I must say I'm disappointed. The boots started falling apart after just a few uses. The stitching came undone, and the sole started detaching. I expected better quality and durability from these boots. I'm not satisfied with my purchase."}], "item_number": 4, "order_number": 17, "description": "TrekReady Hiking Boots, quantity 1, price $140", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Emily \nLast Name: Rodriguez \nAge: 29 \nEmail Address: emilyr@example.com \nPhone Number: 555-111-2222 \nShipping Address: 987 Oak Ave, Cityville USA, 56789 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 3 \ndate: 2023-03-18 \nitem:\n- description: TrailMaster X4 Tent, quantity 3, price $750 \n\u00a0 item_number: 1 \n\norder_number: 12 \ndate: 2023-02-20 \nitem:\n- description: Summit Breeze Jacket, quantity 2, price $240 \n\u00a0 item_number: 3 \n\norder_number: 21 \ndate: 2023-04-02 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 31 \ndate: 2023-04-20 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 39 \ndate: 2023-03-30 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 2, price $220 \n\u00a0 item_number: 11 \n\norder_number: 48 \ndate: 2023-04-16 \nitem:\n- description: MountainDream Sleeping Bag, quantity 2, price $260 \n\u00a0 item_number: 14 \n\norder_number: 61 \ndate: 2023-06-11 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I'm interested in purchasing the BaseCamp Folding Table. Can you provide me with more details about its dimensions and weight? I want to make sure it will fit well in my camping gear."}], "item_number": 5, "order_number": 21, "description": "BaseCamp Folding Table, quantity 1, price $60", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jane \nLast Name: Doe \nAge: 28 \nEmail Address: janedoe@example.com \nPhone Number: 555-987-6543 \nShipping Address: 456 Oak St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 6 \ndate: 2023-01-10 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 15 \ndate: 2023-01-20 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 23 \ndate: 2023-01-30 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 32 \ndate: 2023-02-15 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 44 \ndate: 2023-03-06 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 53 \ndate: 2023-03-21 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 62 \ndate: 2023-04-06 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I recently purchased a camping cooker, but I'm disappointed with its performance. It takes too long to heat up, and the flames seem to be uneven. I expected better quality from this stove."}], "item_number": 6, "order_number": 23, "description": "EcoFire Camping Stove, quantity 1, price $80", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Melissa \nLast Name: Davis \nAge: 31 \nEmail Address: melissad@example.com \nPhone Number: 555-333-4444 \nShipping Address: 789 Ash St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 4 \ndate: 2023-04-22 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 17 \ndate: 2023-03-30 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 25 \ndate: 2023-04-10 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 34 \ndate: 2023-04-25 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 46 \ndate: 2023-05-16 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 55 \ndate: 2023-05-31 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 64 \ndate: 2023-06-16 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I'm interested in purchasing the a camping cooker. Can you tell me more about its fuel efficiency and cooking capacity? I want to make sure it will suit my camping needs."}], "item_number": 6, "order_number": 25, "description": "EcoFire Camping Stove, quantity 1, price $80", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "Hi, I recently bought a sleeping Bag, and I'm having some trouble with the zipper. It seems to get stuck every time I try to close it. What should I do?"}], "item_number": 7, "order_number": 26, "description": "CozyNights Sleeping Bag, quantity 1, price $100", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Amanda \nLast Name: Perez \nAge: 26 \nEmail Address: amandap@example.com \nPhone Number: 555-123-4567 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 5 \ndate: 2023-05-01 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 18 \ndate: 2023-05-04 \nitem:\n- description: TrekReady Hiking Boots, quantity 3, price $420 \n\u00a0 item_number: 4 \n\norder_number: 28 \ndate: 2023-04-15 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 37 \ndate: 2023-04-30 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 58 \ndate: 2023-06-06 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 67 \ndate: 2023-06-21 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I received the sleeping bag I ordered, but it's not the color I chose. I specifically selected the blue one, but I got a green one instead. This is really disappointing!"}], "item_number": 7, "order_number": 28, "description": "CozyNights Sleeping Bag, quantity 2, price $100", "intent": "product exchange"}
{"customer_info": "## Customer_Info\n\nFirst Name: John \nLast Name: Smith \nAge: 35 \nEmail Address: johnsmith@example.com \nPhone Number: 555-123-4567 \nShipping Address: 123 Main St, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 1 \ndate: 2023-01-05 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 19 \ndate: 2023-01-25 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 29 \ndate: 2023-02-10 \nitem:\n- description: Alpine Explorer Tent, quantity 2, price $700 \n\u00a0 item_number: 8 \n\norder_number: 41 \ndate: 2023-03-01 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 50 \ndate: 2023-03-16 \nitem:\n- description: SkyView 2-Person Tent, quantity 2, price $400 \n\u00a0 item_number: 15 \n\norder_number: 59 \ndate: 2023-04-01 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "Hi there! I recently purchased two Tents from your store. They look great, but I wanted to know if they come with any additional accessories like stakes or a rainfly?"}], "item_number": 8, "order_number": 29, "description": "Alpine Explorer Tents, quantity 2, price $700", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Michael \nLast Name: Johnson \nAge: 45 \nEmail Address: michaelj@example.com \nPhone Number: 555-555-1212 \nShipping Address: 789 Elm St, Smallville USA, 34567 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 11 \ndate: 2023-01-15 \nitem:\n- description: Summit Breeze Jacket, quantity 1, price $120 \n\u00a0 item_number: 3 \n\norder_number: 20 \ndate: 2023-02-28 \nitem:\n- description: BaseCamp Folding Table, quantity 2, price $120 \n\u00a0 item_number: 5 \n\norder_number: 30 \ndate: 2023-03-15 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 38 \ndate: 2023-02-25 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 1, price $110 \n\u00a0 item_number: 11 \n\norder_number: 47 \ndate: 2023-03-11 \nitem:\n- description: MountainDream Sleeping Bag, quantity 1, price $130 \n\u00a0 item_number: 14 \n\norder_number: 60 \ndate: 2023-05-06 \nitem:\n- description: TrekStar Hiking Sandals, quantity 2, price $140 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I recently bought a Tent, and I have to say, I'm really disappointed. The tent poles seem flimsy, and the zippers are constantly getting stuck. It's not what I expected from a high-end tent."}], "item_number": 8, "order_number": 30, "description": "Alpine Explorer Tents, quantity 1, price $350", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Emily \nLast Name: Rodriguez \nAge: 29 \nEmail Address: emilyr@example.com \nPhone Number: 555-111-2222 \nShipping Address: 987 Oak Ave, Cityville USA, 56789 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 3 \ndate: 2023-03-18 \nitem:\n- description: TrailMaster X4 Tent, quantity 3, price $750 \n\u00a0 item_number: 1 \n\norder_number: 12 \ndate: 2023-02-20 \nitem:\n- description: Summit Breeze Jacket, quantity 2, price $240 \n\u00a0 item_number: 3 \n\norder_number: 21 \ndate: 2023-04-02 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 31 \ndate: 2023-04-20 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 39 \ndate: 2023-03-30 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 2, price $220 \n\u00a0 item_number: 11 \n\norder_number: 48 \ndate: 2023-04-16 \nitem:\n- description: MountainDream Sleeping Bag, quantity 2, price $260 \n\u00a0 item_number: 14 \n\norder_number: 61 \ndate: 2023-06-11 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I recently received the tent I ordered, but I'm having trouble setting it up. The instructions provided are a bit unclear. Can you guide me through the setup process?"}], "item_number": 8, "order_number": 31, "description": "Alpine Explorer Tents, quantity 1, price $350", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jane \nLast Name: Doe \nAge: 28 \nEmail Address: janedoe@example.com \nPhone Number: 555-987-6543 \nShipping Address: 456 Oak St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 6 \ndate: 2023-01-10 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 15 \ndate: 2023-01-20 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 23 \ndate: 2023-01-30 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 32 \ndate: 2023-02-15 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 44 \ndate: 2023-03-06 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 53 \ndate: 2023-03-21 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 62 \ndate: 2023-04-06 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "Hi, I recently purchased the a backpack from your store. It looks great, but I'm wondering if it has a separate compartment for a hydration bladder?"}], "item_number": 9, "order_number": 32, "description": "SummitClimber Backpack, quantity 1, price $120", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Melissa \nLast Name: Davis \nAge: 31 \nEmail Address: melissad@example.com \nPhone Number: 555-333-4444 \nShipping Address: 789 Ash St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 4 \ndate: 2023-04-22 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 17 \ndate: 2023-03-30 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 25 \ndate: 2023-04-10 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 34 \ndate: 2023-04-25 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 46 \ndate: 2023-05-16 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 55 \ndate: 2023-05-31 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 64 \ndate: 2023-06-16 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I recently received the Backpack I ordered, and I noticed that one of the zippers is not closing smoothly. It seems to get stuck halfway. Is there anything I can do to fix it?"}], "item_number": 9, "order_number": 34, "description": "SummitClimber Backpack, quantity 1, price $120", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently purchased the Pants from your store, but I'm disappointed with the fit. They're too tight around the waist, and the length is too short. Can I exchange them for a different size?"}], "item_number": 10, "order_number": 35, "description": "TrailBlaze Hiking Pants, quantity 1, price $75", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Amanda \nLast Name: Perez \nAge: 26 \nEmail Address: amandap@example.com \nPhone Number: 555-123-4567 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 5 \ndate: 2023-05-01 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 18 \ndate: 2023-05-04 \nitem:\n- description: TrekReady Hiking Boots, quantity 3, price $420 \n\u00a0 item_number: 4 \n\norder_number: 28 \ndate: 2023-04-15 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 37 \ndate: 2023-04-30 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 58 \ndate: 2023-06-06 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 67 \ndate: 2023-06-21 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently received the Pants I ordered, and I noticed that they have several pockets. Can you provide some information on the pocket layout and their intended purposes?"}], "item_number": 10, "order_number": 37, "description": "TrailBlaze Hiking Pants, quantity 1, price $75", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Emily \nLast Name: Rodriguez \nAge: 29 \nEmail Address: emilyr@example.com \nPhone Number: 555-111-2222 \nShipping Address: 987 Oak Ave, Cityville USA, 56789 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 3 \ndate: 2023-03-18 \nitem:\n- description: TrailMaster X4 Tent, quantity 3, price $750 \n\u00a0 item_number: 1 \n\norder_number: 12 \ndate: 2023-02-20 \nitem:\n- description: Summit Breeze Jacket, quantity 2, price $240 \n\u00a0 item_number: 3 \n\norder_number: 21 \ndate: 2023-04-02 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 31 \ndate: 2023-04-20 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 39 \ndate: 2023-03-30 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 2, price $220 \n\u00a0 item_number: 11 \n\norder_number: 48 \ndate: 2023-04-16 \nitem:\n- description: MountainDream Sleeping Bag, quantity 2, price $260 \n\u00a0 item_number: 14 \n\norder_number: 61 \ndate: 2023-06-11 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I purchased two pairs of Hiking Shoes for myself and my husband last month. While my pair is great, my husband's pair seems to be slightly small. Is it possible to exchange them for a larger size?"}], "item_number": 11, "order_number": 39, "description": "TrailWalker Hiking Shoes, quantity 2, price $220", "intent": "product exchange"}
{"customer_info": "## Customer_Info\n\nFirst Name: Daniel \nLast Name: Wilson \nAge: 47 \nEmail Address: danielw@example.com \nPhone Number: 555-444-5555 \nShipping Address: 321 Birch Ln, Smallville USA, 34567 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 9 \ndate: 2023-04-25 \nitem:\n- description: Adventurer Pro Backpack, quantity 3, price $270 \n\u00a0 item_number: 2 \n\norder_number: 13 \ndate: 2023-03-25 \nitem:\n- description: Summit Breeze Jacket, quantity 1, price $120 \n\u00a0 item_number: 3 \n\norder_number: 22 \ndate: 2023-05-07 \nitem:\n- description: BaseCamp Folding Table, quantity 3, price $180 \n\u00a0 item_number: 5 \n\norder_number: 40 \ndate: 2023-04-05 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 1, price $110 \n\u00a0 item_number: 11 \n\norder_number: 49 \ndate: 2023-05-21 \nitem:\n- description: MountainDream Sleeping Bag, quantity 1, price $130 \n\u00a0 item_number: 14 \n\n", "history": [{"role": "customer", "content": "I just bought a pair of Hiking Shoes, and I'm planning a hiking trip soon. Do you have any recommendations for maintaining the shoes and increasing their lifespan?"}], "item_number": 11, "order_number": 40, "description": "TrailWalker Hiking Shoes, quantity 1, price $110", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I'm a Platinum member, and I just ordered two outdoor seats. I was wondering if there is any assembly required and if any tools are needed for that?"}], "item_number": 12, "order_number": 42, "description": "TrekMaster Camping Chair, quantity 2, price $100", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jason \nLast Name: Brown \nAge: 50 \nEmail Address: jasonbrown@example.com \nPhone Number: 555-222-3333 \nShipping Address: 456 Cedar Rd, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 8 \ndate: 2023-03-20 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 27 \ndate: 2023-03-10 \nitem:\n- description: CozyNights Sleeping Bag, quantity 2, price $200 \n\u00a0 item_number: 7 \n\norder_number: 36 \ndate: 2023-03-25 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 2, price $150 \n\u00a0 item_number: 10 \n\norder_number: 43 \ndate: 2023-05-11 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 52 \ndate: 2023-05-26 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 57 \ndate: 2023-05-01 \nitem:\n- description: RainGuard Hiking Jacket, quantity 2, price $220 \n\u00a0 item_number: 17 \n\norder_number: 66 \ndate: 2023-05-16 \nitem:\n- description: CompactCook Camping Stove, quantity 2, price $120 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I bought a camping Chair last month, and it seems to be slightly wobbly when I sit on it. Is there any way to fix this issue?"}], "item_number": 12, "order_number": 43, "description": "TrekMaster Camping Chair, quantity 1, price $50", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: David \nLast Name: Kim \nAge: 42 \nEmail Address: davidkim@example.com \nPhone Number: 555-555-5555 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 7 \ndate: 2023-02-15 \nitem:\n- description: Adventurer Pro Backpack, quantity 2, price $180 \n\u00a0 item_number: 2 \n\norder_number: 16 \ndate: 2023-02-25 \nitem:\n- description: TrekReady Hiking Boots, quantity 2, price $280 \n\u00a0 item_number: 4 \n\norder_number: 24 \ndate: 2023-03-05 \nitem:\n- description: EcoFire Camping Stove, quantity 2, price $160 \n\u00a0 item_number: 6 \n\norder_number: 33 \ndate: 2023-03-20 \nitem:\n- description: SummitClimber Backpack, quantity 2, price $240 \n\u00a0 item_number: 9 \n\norder_number: 45 \ndate: 2023-04-11 \nitem:\n- description: PowerBurner Camping Stove, quantity 2, price $200 \n\u00a0 item_number: 13 \n\norder_number: 54 \ndate: 2023-04-26 \nitem:\n- description: TrailLite Daypack, quantity 2, price $120 \n\u00a0 item_number: 16 \n\norder_number: 63 \ndate: 2023-05-11 \nitem:\n- description: Adventure Dining Table, quantity 2, price $180 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I just bought an outdoor stove but I'm not sure how to attach the fuel canister. Can you please guide me through the process?"}], "item_number": 13, "order_number": 45, "description": "PowerBurner Camping Stove, quantity 2, price $200", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Emily \nLast Name: Rodriguez \nAge: 29 \nEmail Address: emilyr@example.com \nPhone Number: 555-111-2222 \nShipping Address: 987 Oak Ave, Cityville USA, 56789 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 3 \ndate: 2023-03-18 \nitem:\n- description: TrailMaster X4 Tent, quantity 3, price $750 \n\u00a0 item_number: 1 \n\norder_number: 12 \ndate: 2023-02-20 \nitem:\n- description: Summit Breeze Jacket, quantity 2, price $240 \n\u00a0 item_number: 3 \n\norder_number: 21 \ndate: 2023-04-02 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 31 \ndate: 2023-04-20 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 39 \ndate: 2023-03-30 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 2, price $220 \n\u00a0 item_number: 11 \n\norder_number: 48 \ndate: 2023-04-16 \nitem:\n- description: MountainDream Sleeping Bag, quantity 2, price $260 \n\u00a0 item_number: 14 \n\norder_number: 61 \ndate: 2023-06-11 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I've ordered two Sleeping Bags for my upcoming camping trip. I was wondering if they can be zipped together to create a double sleeping bag?"}], "item_number": 14, "order_number": 48, "description": "MountainDream Sleeping Bag, quantity 2, price $260", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Daniel \nLast Name: Wilson \nAge: 47 \nEmail Address: danielw@example.com \nPhone Number: 555-444-5555 \nShipping Address: 321 Birch Ln, Smallville USA, 34567 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 9 \ndate: 2023-04-25 \nitem:\n- description: Adventurer Pro Backpack, quantity 3, price $270 \n\u00a0 item_number: 2 \n\norder_number: 13 \ndate: 2023-03-25 \nitem:\n- description: Summit Breeze Jacket, quantity 1, price $120 \n\u00a0 item_number: 3 \n\norder_number: 22 \ndate: 2023-05-07 \nitem:\n- description: BaseCamp Folding Table, quantity 3, price $180 \n\u00a0 item_number: 5 \n\norder_number: 40 \ndate: 2023-04-05 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 1, price $110 \n\u00a0 item_number: 11 \n\norder_number: 49 \ndate: 2023-05-21 \nitem:\n- description: MountainDream Sleeping Bag, quantity 1, price $130 \n\u00a0 item_number: 14 \n\n", "history": [{"role": "customer", "content": "I recently purchased a Sleeping Bag, and I'm wondering how to properly clean and store it to ensure it lasts for a long time."}], "item_number": 14, "order_number": 49, "description": "MountainDream Sleeping Bag, quantity 1, price $130", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: John \nLast Name: Smith \nAge: 35 \nEmail Address: johnsmith@example.com \nPhone Number: 555-123-4567 \nShipping Address: 123 Main St, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 1 \ndate: 2023-01-05 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 19 \ndate: 2023-01-25 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 29 \ndate: 2023-02-10 \nitem:\n- description: Alpine Explorer Tent, quantity 2, price $700 \n\u00a0 item_number: 8 \n\norder_number: 41 \ndate: 2023-03-01 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 50 \ndate: 2023-03-16 \nitem:\n- description: SkyView 2-Person Tent, quantity 2, price $400 \n\u00a0 item_number: 15 \n\norder_number: 59 \ndate: 2023-04-01 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "I just received my Tents, and they look amazing! I can't wait to use them on our next camping trip. Quick question, though - what's the best way to set up the tent?"}], "item_number": 15, "order_number": 50, "description": "SkyView 2-Person Tent, quantity 2, price $400", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I ordered a Tent, and it arrived yesterday. However, I noticed that one of the tent poles is damaged. How can I get a replacement for the damaged pole?"}], "item_number": 15, "order_number": 51, "description": "SkyView 2-Person Tent, quantity 1, price $200", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jason \nLast Name: Brown \nAge: 50 \nEmail Address: jasonbrown@example.com \nPhone Number: 555-222-3333 \nShipping Address: 456 Cedar Rd, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 8 \ndate: 2023-03-20 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 27 \ndate: 2023-03-10 \nitem:\n- description: CozyNights Sleeping Bag, quantity 2, price $200 \n\u00a0 item_number: 7 \n\norder_number: 36 \ndate: 2023-03-25 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 2, price $150 \n\u00a0 item_number: 10 \n\norder_number: 43 \ndate: 2023-05-11 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 52 \ndate: 2023-05-26 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 57 \ndate: 2023-05-01 \nitem:\n- description: RainGuard Hiking Jacket, quantity 2, price $220 \n\u00a0 item_number: 17 \n\norder_number: 66 \ndate: 2023-05-16 \nitem:\n- description: CompactCook Camping Stove, quantity 2, price $120 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I'm considering buying the SkyView 2-Person Tent for an upcoming camping trip. Can you please tell me more about the tent's ventilation and how it performs in rainy conditions?"}], "item_number": 15, "order_number": 52, "description": "SkyView 2-Person Tent, quantity 1, price $200", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: David \nLast Name: Kim \nAge: 42 \nEmail Address: davidkim@example.com \nPhone Number: 555-555-5555 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 7 \ndate: 2023-02-15 \nitem:\n- description: Adventurer Pro Backpack, quantity 2, price $180 \n\u00a0 item_number: 2 \n\norder_number: 16 \ndate: 2023-02-25 \nitem:\n- description: TrekReady Hiking Boots, quantity 2, price $280 \n\u00a0 item_number: 4 \n\norder_number: 24 \ndate: 2023-03-05 \nitem:\n- description: EcoFire Camping Stove, quantity 2, price $160 \n\u00a0 item_number: 6 \n\norder_number: 33 \ndate: 2023-03-20 \nitem:\n- description: SummitClimber Backpack, quantity 2, price $240 \n\u00a0 item_number: 9 \n\norder_number: 45 \ndate: 2023-04-11 \nitem:\n- description: PowerBurner Camping Stove, quantity 2, price $200 \n\u00a0 item_number: 13 \n\norder_number: 54 \ndate: 2023-04-26 \nitem:\n- description: TrailLite Daypack, quantity 2, price $120 \n\u00a0 item_number: 16 \n\norder_number: 63 \ndate: 2023-05-11 \nitem:\n- description: Adventure Dining Table, quantity 2, price $180 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I purchased two Daypacks for my kids, and while one is perfect, the other has a zipper issue that makes it difficult to open and close. Can I get a replacement for the faulty daypack?"}], "item_number": 16, "order_number": 54, "description": "TrailLite Daypack, quantity 2, price $120", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Melissa \nLast Name: Davis \nAge: 31 \nEmail Address: melissad@example.com \nPhone Number: 555-333-4444 \nShipping Address: 789 Ash St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 4 \ndate: 2023-04-22 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 17 \ndate: 2023-03-30 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 25 \ndate: 2023-04-10 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 34 \ndate: 2023-04-25 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 46 \ndate: 2023-05-16 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 55 \ndate: 2023-05-31 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 64 \ndate: 2023-06-16 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I just bought a TrailLite Daypack for my day hikes, and I was wondering if it's water-resistant. Should I be concerned about my belongings getting wet if it rains?"}], "item_number": 16, "order_number": 55, "description": "TrailLite Daypack, quantity 1, price $60", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jason \nLast Name: Brown \nAge: 50 \nEmail Address: jasonbrown@example.com \nPhone Number: 555-222-3333 \nShipping Address: 456 Cedar Rd, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 8 \ndate: 2023-03-20 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 27 \ndate: 2023-03-10 \nitem:\n- description: CozyNights Sleeping Bag, quantity 2, price $200 \n\u00a0 item_number: 7 \n\norder_number: 36 \ndate: 2023-03-25 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 2, price $150 \n\u00a0 item_number: 10 \n\norder_number: 43 \ndate: 2023-05-11 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 52 \ndate: 2023-05-26 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 57 \ndate: 2023-05-01 \nitem:\n- description: RainGuard Hiking Jacket, quantity 2, price $220 \n\u00a0 item_number: 17 \n\norder_number: 66 \ndate: 2023-05-16 \nitem:\n- description: CompactCook Camping Stove, quantity 2, price $120 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I just bought two RainGuard Hiking Jackets for my wife and me. Can you please provide some care instructions to ensure the jackets maintain their water resistance and durability?"}], "item_number": 17, "order_number": 57, "description": "RainGuard Hiking Jacket, quantity 2, price $220", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Amanda \nLast Name: Perez \nAge: 26 \nEmail Address: amandap@example.com \nPhone Number: 555-123-4567 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 5 \ndate: 2023-05-01 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 18 \ndate: 2023-05-04 \nitem:\n- description: TrekReady Hiking Boots, quantity 3, price $420 \n\u00a0 item_number: 4 \n\norder_number: 28 \ndate: 2023-04-15 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 37 \ndate: 2023-04-30 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 58 \ndate: 2023-06-06 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 67 \ndate: 2023-06-21 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I bought the RainGuard Hiking Jacket a few weeks ago, but I noticed that the seam tape on the inside is starting to peel off. Is there anything I can do to fix this issue?"}], "item_number": 17, "order_number": 58, "description": "RainGuard Hiking Jacket, quantity 1, price $110", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: John \nLast Name: Smith \nAge: 35 \nEmail Address: johnsmith@example.com \nPhone Number: 555-123-4567 \nShipping Address: 123 Main St, Anytown USA, 12345 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 1 \ndate: 2023-01-05 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 19 \ndate: 2023-01-25 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 29 \ndate: 2023-02-10 \nitem:\n- description: Alpine Explorer Tent, quantity 2, price $700 \n\u00a0 item_number: 8 \n\norder_number: 41 \ndate: 2023-03-01 \nitem:\n- description: TrekMaster Camping Chair, quantity 1, price $50 \n\u00a0 item_number: 12 \n\norder_number: 50 \ndate: 2023-03-16 \nitem:\n- description: SkyView 2-Person Tent, quantity 2, price $400 \n\u00a0 item_number: 15 \n\norder_number: 59 \ndate: 2023-04-01 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "Hi, I purchased the TrekStar Hiking Sandals a few weeks ago, and they feel a bit tight. Is there a break-in period for these sandals, or should I exchange them for a larger size?"}], "item_number": 18, "order_number": 59, "description": "TrekStar Hiking Sandals, quantity 1, price $70", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Emily \nLast Name: Rodriguez \nAge: 29 \nEmail Address: emilyr@example.com \nPhone Number: 555-111-2222 \nShipping Address: 987 Oak Ave, Cityville USA, 56789 \nMembership: None \n\n## Recent_Purchases\n\norder_number: 3 \ndate: 2023-03-18 \nitem:\n- description: TrailMaster X4 Tent, quantity 3, price $750 \n\u00a0 item_number: 1 \n\norder_number: 12 \ndate: 2023-02-20 \nitem:\n- description: Summit Breeze Jacket, quantity 2, price $240 \n\u00a0 item_number: 3 \n\norder_number: 21 \ndate: 2023-04-02 \nitem:\n- description: BaseCamp Folding Table, quantity 1, price $60 \n\u00a0 item_number: 5 \n\norder_number: 31 \ndate: 2023-04-20 \nitem:\n- description: Alpine Explorer Tent, quantity 1, price $350 \n\u00a0 item_number: 8 \n\norder_number: 39 \ndate: 2023-03-30 \nitem:\n- description: TrailWalker Hiking Shoes, quantity 2, price $220 \n\u00a0 item_number: 11 \n\norder_number: 48 \ndate: 2023-04-16 \nitem:\n- description: MountainDream Sleeping Bag, quantity 2, price $260 \n\u00a0 item_number: 14 \n\norder_number: 61 \ndate: 2023-06-11 \nitem:\n- description: TrekStar Hiking Sandals, quantity 1, price $70 \n\u00a0 item_number: 18 \n\n", "history": [{"role": "customer", "content": "Hi there, I'm interested in purchasing the TrekStar Hiking Sandals. Can you tell me more about their features?"}], "item_number": 18, "order_number": 61, "description": "TrekStar Hiking Sandals, quantity 1, price $70", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Jane \nLast Name: Doe \nAge: 28 \nEmail Address: janedoe@example.com \nPhone Number: 555-987-6543 \nShipping Address: 456 Oak St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 6 \ndate: 2023-01-10 \nitem:\n- description: Adventurer Pro Backpack, quantity 1, price $90 \n\u00a0 item_number: 2 \n\norder_number: 15 \ndate: 2023-01-20 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 23 \ndate: 2023-01-30 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 32 \ndate: 2023-02-15 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 44 \ndate: 2023-03-06 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 53 \ndate: 2023-03-21 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 62 \ndate: 2023-04-06 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I recently purchased the Adventure Dining Table, but I'm not happy with its quality. The table arrived with scratches on the surface, and one of the legs seems wobbly. I expected better craftsmanship from your brand. This is disappointing."}], "item_number": 19, "order_number": 62, "description": "Adventure Dining Table, quantity 1, price $90", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Melissa \nLast Name: Davis \nAge: 31 \nEmail Address: melissad@example.com \nPhone Number: 555-333-4444 \nShipping Address: 789 Ash St, Another City USA, 67890 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 4 \ndate: 2023-04-22 \nitem:\n- description: TrailMaster X4 Tent, quantity 2, price $500 \n\u00a0 item_number: 1 \n\norder_number: 17 \ndate: 2023-03-30 \nitem:\n- description: TrekReady Hiking Boots, quantity 1, price $140 \n\u00a0 item_number: 4 \n\norder_number: 25 \ndate: 2023-04-10 \nitem:\n- description: EcoFire Camping Stove, quantity 1, price $80 \n\u00a0 item_number: 6 \n\norder_number: 34 \ndate: 2023-04-25 \nitem:\n- description: SummitClimber Backpack, quantity 1, price $120 \n\u00a0 item_number: 9 \n\norder_number: 46 \ndate: 2023-05-16 \nitem:\n- description: PowerBurner Camping Stove, quantity 1, price $100 \n\u00a0 item_number: 13 \n\norder_number: 55 \ndate: 2023-05-31 \nitem:\n- description: TrailLite Daypack, quantity 1, price $60 \n\u00a0 item_number: 16 \n\norder_number: 64 \ndate: 2023-06-16 \nitem:\n- description: Adventure Dining Table, quantity 1, price $90 \n\u00a0 item_number: 19 \n\n", "history": [{"role": "customer", "content": "I recently purchased the Adventure Dining Table, and I have a question about its setup. The instructions provided are not very clear, and I'm having trouble assembling the table correctly. Can you provide me with some guidance or more detailed instructions?"}], "item_number": 19, "order_number": 64, "description": "Adventure Dining Table, quantity 2, price $180", "intent": "product question"}
{"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently purchased the CompactCook Camping Stove, and I'm quite disappointed with its performance. The flame doesn't seem to stay consistent, and it takes forever to boil water. This is not what I expected from a camping stove. Can you help me with this issue?"}], "item_number": 20, "order_number": 65, "description": "CompactCook Camping Stove, quantity 1, price $60", "intent": "product return"}
{"customer_info": "## Customer_Info\n\nFirst Name: Amanda \nLast Name: Perez \nAge: 26 \nEmail Address: amandap@example.com \nPhone Number: 555-123-4567 \nShipping Address: 654 Pine St, Suburbia USA, 23456 \nMembership: Gold \n\n## Recent_Purchases\n\norder_number: 5 \ndate: 2023-05-01 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 18 \ndate: 2023-05-04 \nitem:\n- description: TrekReady Hiking Boots, quantity 3, price $420 \n\u00a0 item_number: 4 \n\norder_number: 28 \ndate: 2023-04-15 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 37 \ndate: 2023-04-30 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 58 \ndate: 2023-06-06 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 67 \ndate: 2023-06-21 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n", "history": [{"role": "customer", "content": "I recently received the CompactCook Camping Stove, and I'm not sure about its maintenance requirements. Are there any specific cleaning or storage instructions I should follow to ensure the stove's longevity?"}], "item_number": 20, "order_number": 67, "description": "CompactCook Camping Stove, quantity 1, price $60", "intent": "product question"}
@@ -0,0 +1,13 @@
{
"customer_info": "## Customer_Info\n\nFirst Name: Sarah \nLast Name: Lee \nAge: 38 \nEmail Address: sarahlee@example.com \nPhone Number: 555-867-5309 \nShipping Address: 321 Maple St, Bigtown USA, 90123 \nMembership: Platinum \n\n## Recent_Purchases\n\norder_number: 2 \ndate: 2023-02-10 \nitem:\n- description: TrailMaster X4 Tent, quantity 1, price $250 \n\u00a0 item_number: 1 \n\norder_number: 26 \ndate: 2023-02-05 \nitem:\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \n\u00a0 item_number: 7 \n\norder_number: 35 \ndate: 2023-02-20 \nitem:\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \n\u00a0 item_number: 10 \n\norder_number: 42 \ndate: 2023-04-06 \nitem:\n- description: TrekMaster Camping Chair, quantity 2, price $100 \n\u00a0 item_number: 12 \n\norder_number: 51 \ndate: 2023-04-21 \nitem:\n- description: SkyView 2-Person Tent, quantity 1, price $200 \n\u00a0 item_number: 15 \n\norder_number: 56 \ndate: 2023-03-26 \nitem:\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \n\u00a0 item_number: 17 \n\norder_number: 65 \ndate: 2023-04-11 \nitem:\n- description: CompactCook Camping Stove, quantity 1, price $60 \n\u00a0 item_number: 20 \n\n",
"history": [
{
"role": "customer",
"content": "I recently bought the TrailMaster X4 Tent, and it leaked during a light rain. This is unacceptable! I expected a reliable and waterproof tent, but it failed to deliver. I'm extremely disappointed in the quality."
}
],
"item_number": 1,
"order_number": 2,
"description": "TrailMaster X4 Tent, quantity 1, price $250",
"intent": "product return"
}
@@ -0,0 +1,19 @@
import os
from promptflow.core import tool
from promptflow.connections import CustomConnection
from intent import extract_intent
@tool
def extract_intent_tool(chat_prompt, connection: CustomConnection) -> str:
# set environment variables
for key, value in dict(connection).items():
os.environ[key] = value
# call the entry function
return extract_intent(
chat_prompt=chat_prompt,
)
@@ -0,0 +1,29 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
history:
type: string
customer_info:
type: string
outputs:
output:
type: string
reference: ${extract_intent.output}
nodes:
- name: chat_prompt
type: prompt
source:
type: code
path: user_intent_zero_shot.jinja2
inputs: # Please check the generated prompt inputs
history: ${inputs.history}
customer_info: ${inputs.customer_info}
- name: extract_intent
type: python
source:
type: code
path: extract_intent_tool.py
inputs:
chat_prompt: ${chat_prompt.output}
connection: custom_connection
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,4 @@
{
"customer_info": "## Customer_Info\\n\\nFirst Name: Sarah \\nLast Name: Lee \\nAge: 38 \\nEmail Address: sarahlee@example.com \\nPhone Number: 555-867-5309 \\nShipping Address: 321 Maple St, Bigtown USA, 90123 \\nMembership: Platinum \\n\\n## Recent_Purchases\\n\\norder_number: 2 \\ndate: 2023-02-10 \\nitem:\\n- description: TrailMaster X4 Tent, quantity 1, price $250 \\n\\u00a0 item_number: 1 \\n\\norder_number: 26 \\ndate: 2023-02-05 \\nitem:\\n- description: CozyNights Sleeping Bag, quantity 1, price $100 \\n\\u00a0 item_number: 7 \\n\\norder_number: 35 \\ndate: 2023-02-20 \\nitem:\\n- description: TrailBlaze Hiking Pants, quantity 1, price $75 \\n\\u00a0 item_number: 10 \\n\\norder_number: 42 \\ndate: 2023-04-06 \\nitem:\\n- description: TrekMaster Camping Chair, quantity 2, price $100 \\n\\u00a0 item_number: 12 \\n\\norder_number: 51 \\ndate: 2023-04-21 \\nitem:\\n- description: SkyView 2-Person Tent, quantity 1, price $200 \\n\\u00a0 item_number: 15 \\n\\norder_number: 56 \\ndate: 2023-03-26 \\nitem:\\n- description: RainGuard Hiking Jacket, quantity 1, price $110 \\n\\u00a0 item_number: 17 \\n\\norder_number: 65 \\ndate: 2023-04-11 \\nitem:\\n- description: CompactCook Camping Stove, quantity 1, price $60 \\n\\u00a0 item_number: 20 \\n\\n",
"history": "[ { \"role\": \"customer\", \"content\": \"I recently bought the TrailMaster X4 Tent, and it leaked during a light rain. This is unacceptable! I expected a reliable and waterproof tent, but it failed to deliver. I'm extremely disappointed in the quality.\" } ]"
}
@@ -0,0 +1,81 @@
import os
import pip
from langchain.chat_models import AzureChatOpenAI
from langchain.prompts.chat import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.prompts.prompt import PromptTemplate
from langchain.schema import HumanMessage
def extract_intent(chat_prompt: str):
if (
"AZURE_OPENAI_API_KEY" not in os.environ
or "AZURE_OPENAI_API_BASE" not in os.environ
):
# load environment variables from .env file
try:
from dotenv import load_dotenv
except ImportError:
# This can be removed if user using custom image.
pip.main(["install", "python-dotenv"])
from dotenv import load_dotenv
load_dotenv()
# AZURE_OPENAI_ENDPOINT conflict with AZURE_OPENAI_API_BASE when use with langchain
if "AZURE_OPENAI_ENDPOINT" in os.environ:
os.environ.pop("AZURE_OPENAI_ENDPOINT")
chat = AzureChatOpenAI(
deployment_name=os.environ.get("CHAT_DEPLOYMENT_NAME", "gpt-35-turbo"),
openai_api_key=os.environ["AZURE_OPENAI_API_KEY"],
openai_api_base=os.environ["AZURE_OPENAI_API_BASE"],
openai_api_type="azure",
openai_api_version="2023-07-01-preview",
temperature=0,
)
reply_message = chat([HumanMessage(content=chat_prompt)])
return reply_message.content
def generate_prompt(customer_info: str, history: list, user_prompt_template: str):
chat_history_text = "\n".join(
[message["role"] + ": " + message["content"] for message in history]
)
prompt_template = PromptTemplate.from_template(user_prompt_template)
chat_prompt_template = ChatPromptTemplate.from_messages(
[HumanMessagePromptTemplate(prompt=prompt_template)]
)
return chat_prompt_template.format_prompt(
customer_info=customer_info, chat_history=chat_history_text
).to_string()
if __name__ == "__main__":
import json
with open("./data/denormalized-flat.jsonl", "r") as f:
data = [json.loads(line) for line in f.readlines()]
# only ten samples
data = data[:10]
# load template from file
with open("user_intent_zero_shot.jinja2", "r") as f:
user_prompt_template = f.read()
# each test
for item in data:
chat_prompt = generate_prompt(
item["customer_info"], item["history"], user_prompt_template
)
reply = extract_intent(chat_prompt)
print("=====================================")
# print("Customer info: ", item["customer_info"])
# print("+++++++++++++++++++++++++++++++++++++")
print("Chat history: ", item["history"])
print("+++++++++++++++++++++++++++++++++++++")
print(reply)
print("+++++++++++++++++++++++++++++++++++++")
print(f"Ground Truth: {item['intent']}")
print("=====================================")
@@ -0,0 +1,5 @@
promptflow
promptflow-tools
python-dotenv
langchain<0.2.0
jinja2
@@ -0,0 +1,43 @@
You are given a list of orders with item_numbers from a customer and a statement from the customer. It is your job to identify
the intent that the customer has with their statement. Possible intents can be:
"product return", "product exchange", "general question", "product question", "other".
If the intent is product related ("product return", "product exchange", "product question"), then you should also
provide the order id and item that the customer is referring to in their statement.
For instance if you are give the following list of orders:
order_number: 2020230
date: 2023-04-23
store_location: SeattleStore
items:
- description: Roof Rack, color black, price $199.99
item_number: 101010
- description: Running Shoes, size 10, color blue, price $99.99
item_number: 202020
You are given the following customer statements:
- I am having issues with the jobbing shoes I bought.
Then you should answer with in valid yaml format with the fields intent, order_number, item, and item_number like so:
intent: product question
order_number: 2020230
descrption: Running Shoes, size 10, color blue, price $99.99
item_number: 202020
Here is the actual problem you need to solve:
In triple backticks below is the customer information and a list of orders.
```
{{customer_info}}
```
In triple backticks below are the is the chat history with customer statements and replies from the customer service agent:
```
{{chat_history}}
```
What is the customer's `intent:` here?
"product return", "exchange product", "general question", "product question" or "other"?
Reply with only the intent string.
@@ -0,0 +1,23 @@
You are given a list of orders with item_numbers from a customer and a statement from the customer. It is your job to identify the intent that the customer has with their statement. Possible intents can be: "product return", "product exchange", "general question", "product question", "other".
In triple backticks below is the customer information and a list of orders.
```
{{customer_info}}
```
In triple backticks below are the is the chat history with customer statements and replies from the customer service agent:
```
{{history}}
```
What is the customer's `intent:` here?
"product return", "exchange product", "general question", "product question" or "other"?
Reply with only the intent string.
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-4o
@@ -0,0 +1,2 @@
{"question": "How many colors are there in the image?", "input_image": {"data:image/png;url": "https://developer.microsoft.com/_devcom/images/logo-ms-social.png"}}
{"question": "What's this image about?", "input_image": {"data:image/png;url": "https://developer.microsoft.com/_devcom/images/404.png"}}
@@ -0,0 +1,5 @@
agent-framework>=1.0.1
agent-framework-openai>=1.0.1
python-dotenv
Pillow
requests

Some files were not shown because too many files have changed in this diff Show More