-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Include a search example designed with UI and dynamiq code integration.
- Loading branch information
Showing
6 changed files
with
408 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
## Search use - case | ||
|
||
This project demonstrates two different approaches to building a web-based search application powered by **Dynamiq**. The main goal is to showcase how Dynamiq's workflows, agents, and tools can be used in a flexible manner to process user queries and provide synthesized, accurate search results in real-time. | ||
|
||
The project has two stages: | ||
1. **Stage 1**: Building the logic server programmatically using Dynamiq code, where we define nodes, agents, and tools, and then craft a workflow. The workflow is then integrated into a frontend app using Streamlit to create an interactive web search tool. | ||
2. **Stage 2**: Using the **Dynamiq UI** to craft nodes, agents, and tools visually and deploy the logic via an API endpoint. This endpoint is then used as the backend for a Streamlit-based web application. | ||
|
||
--- | ||
|
||
## Project Structure | ||
|
||
``` | ||
README.md | ||
app.py | ||
run.sh | ||
server.py | ||
server_via_dynamiq.py | ||
``` | ||
|
||
### Files Overview | ||
|
||
- **README.md**: This file. | ||
- **app.py**: The main entry point for the Streamlit app. It handles query input, processes queries using the server logic, and displays results in real-time. | ||
- **run.sh**: A script to launch the application (optional, depending on setup). | ||
- **server.py**: Contains the server logic for **Stage 1**, where Dynamiq workflows are defined programmatically. This server rephrases user queries, searches for results using a search tool, and synthesizes answers. | ||
- **server_via_dynamiq.py**: A simplified server for **Stage 2**, which uses the Dynamiq UI to set up nodes, agents, and tools, and leverages an API endpoint to process queries. | ||
|
||
--- | ||
|
||
## Stage 1: Programmatically Building the Search Application | ||
|
||
In this stage, we manually define the logic server using Dynamiq's Python library. | ||
|
||
### Key Components: | ||
|
||
1. **Agents and Tools**: | ||
- **Rephrasing Agent**: This agent rewrites the user's input query to make it more concise and optimized for search engines. | ||
- **Search Tool**: This tool uses an external search engine (e.g., SERP Scale) to retrieve relevant information based on the rephrased query. | ||
- **Answer Synthesizer Agent**: This agent synthesizes an answer from the search results and formats it according to the query. | ||
|
||
2. **Workflow**: | ||
- A workflow is defined using the agents and tools mentioned above. The query first goes through the rephrasing agent, then to the search tool, and finally to the answer synthesizer agent. | ||
|
||
3. **Frontend**: | ||
- A Streamlit-based frontend (`app.py`) allows users to input search queries, and it processes these queries through the defined workflow. The result is streamed back to the user in real time, showing both the sources of information and the final answer. | ||
|
||
### How to Run: | ||
|
||
Run the Streamlit app: | ||
``` | ||
streamlit run app.py | ||
``` | ||
|
||
--- | ||
|
||
## Stage 2: Building the Application Using Dynamiq UI | ||
|
||
In this stage, we utilize the **Dynamiq UI** to visually create the workflow, agents, and tools. Once the workflow is set up, it is deployed as an API, which acts as a backend for the Streamlit app. | ||
|
||
### Key Components: | ||
|
||
1. **Dynamiq UI**: | ||
- The agents and tools are created using Dynamiq's graphical interface. | ||
- The workflow is deployed, and an API endpoint is provided to interact with the workflow. | ||
|
||
2. **Backend**: | ||
- The `server_via_dynamiq.py` file contains a simplified implementation that connects to the Dynamiq API. The queries are sent to this API, and the results are streamed back and displayed to the user in real time. | ||
|
||
### How to Run: | ||
|
||
1. Set up environment variables for the Dynamiq API: | ||
``` | ||
export DYNAMIQ_ENDPOINT=<Your Dynamiq endpoint> | ||
export DYNAMIQ_API_KEY=<Your Dynamiq API key> | ||
``` | ||
|
||
2. Run the Streamlit app: | ||
``` | ||
streamlit run app.py | ||
``` | ||
|
||
--- | ||
|
||
## Conclusion | ||
|
||
This project showcases two flexible approaches to building a search-based application using Dynamiq: | ||
|
||
- In **Stage 1**, we manually define the workflow and integrate it into a Python-based backend. | ||
- In **Stage 2**, we leverage the Dynamiq UI to deploy the backend as an API. | ||
|
||
Both approaches allow for real-time query processing and answer synthesis, demonstrating the power and versatility of Dynamiq in creating intelligent search tools. | ||
|
||
Feel free to experiment with both approaches depending on your needs and preferences! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import time | ||
import streamlit as st | ||
from examples.use_case_search.server import process_query | ||
|
||
|
||
def reset_conversation(): | ||
""" | ||
Resets the conversation state, clearing query history and input fields. | ||
""" | ||
st.session_state.query_history = [] | ||
st.session_state.current_query = "" | ||
st.session_state.clear_input = False | ||
|
||
|
||
def initialize_session_state(): | ||
""" | ||
Initializes session state variables if not already set. | ||
""" | ||
if "query_history" not in st.session_state: | ||
st.session_state.query_history = [] | ||
if "current_query" not in st.session_state: | ||
st.session_state.current_query = "" | ||
if "clear_input" not in st.session_state: | ||
st.session_state.clear_input = False | ||
|
||
|
||
def handle_query_submission(query: str): | ||
""" | ||
Handles query submission and displays the result chunk by chunk. | ||
Args: | ||
query (str): The user input query. | ||
""" | ||
if query.strip(): | ||
st.session_state.query_history.append(query) | ||
st.session_state.current_query = query | ||
|
||
result_placeholder = st.empty() | ||
|
||
with st.spinner("Processing your query..."): | ||
result_text = "" | ||
for chunk in process_query(query): | ||
result_text += chunk + " " | ||
result_placeholder.write(result_text.strip()) | ||
time.sleep(0.05) | ||
|
||
st.session_state.clear_input = True | ||
else: | ||
st.warning("Please enter a query before submitting.") | ||
|
||
|
||
def main(): | ||
st.title("Search Application") | ||
|
||
initialize_session_state() | ||
|
||
query = st.text_input("Enter your query:", value="", key="initial_input") | ||
|
||
if st.button("Submit"): | ||
handle_query_submission(query) | ||
|
||
if st.button("Start New Query"): | ||
reset_conversation() | ||
st.experimental_rerun() | ||
|
||
if st.session_state.clear_input: | ||
st.session_state.clear_input = False | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#!/bin/bash | ||
|
||
python -m streamlit run examples/use_case_search/app.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import re | ||
|
||
from dynamiq import Workflow | ||
from dynamiq.callbacks import TracingCallbackHandler | ||
from dynamiq.connections import ScaleSerp | ||
from dynamiq.flows import Flow | ||
from dynamiq.nodes import InputTransformer | ||
from dynamiq.nodes.agents.simple import SimpleAgent | ||
from dynamiq.nodes.tools.scale_serp import ScaleSerpTool | ||
from dynamiq.runnables import RunnableConfig | ||
from dynamiq.utils.logger import logger | ||
from examples.llm_setup import setup_llm | ||
|
||
|
||
def extract_tag_content(text, tag): | ||
""" | ||
Extract content wrapped within specific XML-like tags from the text. | ||
Args: | ||
text (str): The input text containing the tag. | ||
tag (str): The tag name to extract content from. | ||
Returns: | ||
str: The content inside the tag if found, otherwise None. | ||
""" | ||
pattern = rf"<{tag}>(.*?)</{tag}>" | ||
match = re.search(pattern, text, re.DOTALL) | ||
return match.group(1).strip() if match else None | ||
|
||
|
||
AGENT_QUERY_ROLE = """ | ||
You are an AI assistant tasked with processing and refactoring search queries. | ||
Your goal is to rewrite queries to be more concise, clear, and useful for search engines. Follow these guidelines: | ||
1. Remove unnecessary words like "what is," "who is," "where is," etc. | ||
2. Convert questions into declarative statements | ||
3. Focus on the core subject of the query | ||
4. Maintain essential keywords | ||
5. Ensure the refactored query is grammatically correct | ||
Here are some examples of original queries and their refactored versions: | ||
Original: "Who was the first person to walk on the moon?" | ||
Refactored: "First person to walk on the moon" | ||
Original: "What are the ingredients in a chocolate chip cookie?" | ||
Refactored: "Chocolate chip cookie ingredients" | ||
Original: "How tall is the Eiffel Tower?" | ||
Refactored: "Eiffel Tower height" | ||
Rewrite the query according to the guidelines provided. Output your refactored version, without any additional wording. | ||
""" # noqa E501 | ||
AGENT_ANSWER_ROLE = """ | ||
You are an AI assistant tasked with synthesizing answers from search results. | ||
Your goal is to provide a concise and informative answer based on the following search results and user query. | ||
Here are the search results: | ||
<search_results> | ||
{search_results} | ||
</search_results> | ||
And here is the user's query: | ||
<user_query> | ||
{user_query} | ||
</user_query> | ||
To complete this task, follow these steps: | ||
1. Carefully read through the search results and identify the most relevant information that addresses the user's query. | ||
2. Synthesize the information from multiple sources to create a comprehensive and accurate answer. | ||
3. As you craft your answer, cite your sources using numbered markdown links formatted as [1], [2], etc. Each citation should correspond to a source in your source list. | ||
4. Write your answer in a clear, concise, and journalistic tone. Avoid unnecessary jargon and explain any complex concepts in simple terms. | ||
5. Ensure that your answer is well-structured and easy to read. Use paragraphs to separate different ideas or aspects of the answer. | ||
6. After your main answer, provide a numbered list of sources. Format each source as follows: | ||
[number]. Source name (source URL) | ||
Provide your synthesized answer within <answer> tags, followed by the source list within <sources> tags using link formatting like in markdown. | ||
Your response should look like this: | ||
<answer> | ||
Your synthesized answer here, with citations like this [1] and this [2]. | ||
</answer> | ||
<sources> | ||
1. [Source Name 1](http://www.example1.com) | ||
2. [Source Name 2](http://www.example2.com) | ||
</sources> | ||
Remember to focus on accuracy, clarity, and proper citation in your response. | ||
If there are errors with the query or you are unable to craft a response, provide polite feedback within the <answer> tags. | ||
Explain that you are not able to find the answer and provide some suggestions for the user to improve the query. | ||
""" # noqa E501 | ||
|
||
# Setup models | ||
llm_mini = setup_llm(model_provider="gpt", model_name="gpt-4o-mini", max_tokens=500, temperature=0.5) | ||
llm = setup_llm(model_provider="gpt", model_name="gpt-4o", max_tokens=3000, temperature=0.1) | ||
# Define agents and search tool | ||
agent_query_rephraser = SimpleAgent( | ||
id="agent_query_rephraser", | ||
name="agent_query_rephraser", | ||
role=AGENT_QUERY_ROLE, | ||
llm=llm_mini, | ||
) | ||
|
||
search_tool = ScaleSerpTool( | ||
name="search_tool", | ||
id="search_tool", | ||
connection=ScaleSerp(params={"location": "USA, United Kingdom, Europe"}), | ||
limit=5, | ||
is_optimized_for_agents=True, | ||
).depends_on(agent_query_rephraser) | ||
search_tool.input_transformer = InputTransformer(selector={"input": "$[agent_query_rephraser].output.content"}) | ||
|
||
agent_answer_synthesizer = SimpleAgent( | ||
id="agent_answer_synthesizer", | ||
name="agent_answer_synthesizer", | ||
role=AGENT_ANSWER_ROLE, | ||
llm=llm, | ||
).depends_on([search_tool, agent_query_rephraser]) | ||
agent_answer_synthesizer._prompt_variables.update({"search_results": "{search_results}", "user_query": "{user_query}"}) | ||
agent_answer_synthesizer.input_transformer = InputTransformer( | ||
selector={ | ||
"input": "", | ||
"search_results": "$[search_tool].output.content", | ||
"user_query": "$[agent_query_rephraser].output.content", | ||
} | ||
) | ||
|
||
tracing = TracingCallbackHandler() | ||
wf = Workflow(flow=Flow(nodes=[agent_query_rephraser, search_tool, agent_answer_synthesizer]), callbacks=[tracing]) | ||
|
||
|
||
def process_query(query: str): | ||
""" | ||
Process the user's query through a workflow that rephrases, searches, and synthesizes an answer. | ||
Yields results chunk by chunk to simulate real-time streaming of data. | ||
Args: | ||
query (str): The original user query. | ||
Yields: | ||
str: Chunks of the final result (sources and answer). | ||
""" | ||
try: | ||
# Run the workflow with the provided query | ||
result = wf.run(input_data={"input": query}, config=RunnableConfig(callbacks=[tracing])) | ||
|
||
output_content = result.output[agent_answer_synthesizer.id]["output"].get("content") | ||
logger.info(f"Workflow result: {output_content}") | ||
|
||
answer = extract_tag_content(output_content, "answer") | ||
sources = extract_tag_content(output_content, "sources") | ||
|
||
if answer and sources: | ||
# Stream the sources first | ||
yield "Sources:\n\n" | ||
for source_chunk in sources.split("\n"): | ||
yield source_chunk + "\n\n" | ||
|
||
# Stream the answer next | ||
yield "\n\nAnswer:\n\n" | ||
for answer_chunk in answer.split(" "): | ||
yield answer_chunk + " " | ||
else: | ||
yield "Error: Unable to extract answer or sources from the workflow output." | ||
|
||
except Exception as e: | ||
logger.error(f"An error occurred while processing the query: {e}") | ||
yield f"Error: {str(e)}" |
Oops, something went wrong.
b02820e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage Report