diff --git a/actions/web_scrape.py b/actions/web_scrape.py index 06d8eadf5..7352d2179 100644 --- a/actions/web_scrape.py +++ b/actions/web_scrape.py @@ -136,6 +136,8 @@ def scrape_text_with_selenium(url: str) -> tuple[WebDriver, str]: "prefs", {"download_restrictions": 3} ) driver = webdriver.Chrome(options=options) + + print(f"scraping url {url}...") driver.get(url) WebDriverWait(driver, 10).until( diff --git a/agent/llm_utils.py b/agent/llm_utils.py index e36a2535a..ed432ebee 100644 --- a/agent/llm_utils.py +++ b/agent/llm_utils.py @@ -98,7 +98,7 @@ async def stream_response(model, messages, temperature, max_tokens, websocket): return response -def choose_agent(task: str) -> str: +def choose_agent(task: str) -> dict: """Determines what agent should be used Args: task (str): The research question the user asked diff --git a/agent/prompts.py b/agent/prompts.py index 3913fd1ba..c8405ced6 100644 --- a/agent/prompts.py +++ b/agent/prompts.py @@ -1,3 +1,4 @@ +from datetime import datetime def generate_agent_role_prompt(agent): """ Generates the agent role prompt. Args: agent (str): The type of the agent. @@ -25,9 +26,10 @@ def generate_report_prompt(question, research_summary): return f'"""{research_summary}""" Using the above information, answer the following'\ f' question or topic: "{question}" in a detailed report --'\ " The report should focus on the answer to the question, should be well structured, informative," \ - " in depth, with facts and numbers if available, a minimum of 1,200 words and with markdown syntax and apa format. "\ - "You MUST determine your own concrete and valid opinion based on the given information. Do NOT deter to general and meaningless conclusions." \ - "Write all used source urls at the end of the report in apa format" + " in depth, with facts and numbers if available, a minimum of 1,200 words and with markdown syntax and apa format.\n "\ + "You MUST determine your own concrete and valid opinion based on the given information. Do NOT deter to general and meaningless conclusions.\n" \ + f"Write all used source urls at the end of the report in apa format.\n " \ + f"Assume that the current date is {datetime.now().strftime('%B %d, %Y')}" def generate_search_queries_prompt(question): """ Generates the search queries prompt for the given question. @@ -35,8 +37,8 @@ def generate_search_queries_prompt(question): Returns: str: The search queries prompt for the given question """ - return f'Write 4 google search queries to search online that form an objective opinion from the following: "{question}"'\ - f'You must respond with a list of strings in the following format: ["query 1", "query 2", "query 3", "query 4"]' + return f'Write 3 google search queries to search online that form an objective opinion from the following: "{question}"'\ + f'You must respond with a list of strings in the following format: ["query 1", "query 2", "query 3"]' def generate_resource_report_prompt(question, research_summary): diff --git a/agent/research_agent.py b/agent/research_agent.py index 9c0d95f16..17de24d0b 100644 --- a/agent/research_agent.py +++ b/agent/research_agent.py @@ -3,7 +3,7 @@ # libraries import asyncio import json -import uuid +import hashlib from actions.web_search import web_search from actions.web_scrape import async_browse @@ -23,7 +23,7 @@ class ResearchAgent: - def __init__(self, question, agent, agent_role_prompt, websocket): + def __init__(self, question, agent, agent_role_prompt, websocket=None): """ Initializes the research assistant with the given question. Args: question (str): The question to research Returns: None @@ -34,10 +34,14 @@ def __init__(self, question, agent, agent_role_prompt, websocket): self.agent_role_prompt = agent_role_prompt if agent_role_prompt else prompts.generate_agent_role_prompt(agent) self.visited_urls = set() self.research_summary = "" - self.directory_name = uuid.uuid4() - self.dir_path = os.path.dirname(f"./outputs/{self.directory_name}/") + self.dir_path = f"./outputs/{hashlib.sha1(question.encode()).hexdigest()}" self.websocket = websocket + async def stream_output(self, output): + if not self.websocket: + return print(output) + await self.websocket.send_json({"type": "logs", "output": output}) + async def summarize(self, text, topic): """ Summarizes the given text for the given topic. @@ -47,7 +51,7 @@ async def summarize(self, text, topic): """ messages = [create_message(text, topic)] - await self.websocket.send_json({"type": "logs", "output": f"📝 Summarizing text for query: {text}"}) + await self.stream_output(f"📝 Summarizing text for query: {text}") return create_chat_completion( model=CFG.fast_llm_model, @@ -63,7 +67,8 @@ async def get_new_urls(self, url_set_input): new_urls = [] for url in url_set_input: if url not in self.visited_urls: - await self.websocket.send_json({"type": "logs", "output": f"✅ Adding source url to research: {url}\n"}) + await self.stream_output(f"✅ Adding source url to research: {url}\n") + self.visited_urls.add(url) new_urls.append(url) @@ -91,8 +96,7 @@ async def create_search_queries(self): Returns: list[str]: The search queries for the given question """ result = await self.call_agent(prompts.generate_search_queries_prompt(self.question)) - print(result) - await self.websocket.send_json({"type": "logs", "output": f"🧠 I will conduct my research based on the following queries: {result}..."}) + await self.stream_output(f"🧠 I will conduct my research based on the following queries: {result}...") return json.loads(result) async def async_search(self, query): @@ -103,8 +107,7 @@ async def async_search(self, query): search_results = json.loads(web_search(query)) new_search_urls = self.get_new_urls([url.get("href") for url in search_results]) - await self.websocket.send_json( - {"type": "logs", "output": f"🌐 Browsing the following sites for relevant information: {new_search_urls}..."}) + await self.stream_output(f"🌐 Browsing the following sites for relevant information: {new_search_urls}...") # Create a list to hold the coroutine objects tasks = [async_browse(url, query, self.websocket) for url in await new_search_urls] @@ -120,13 +123,13 @@ async def run_search_summary(self, query): Returns: str: The search summary for the given query """ - await self.websocket.send_json({"type": "logs", "output": f"🔎 Running research for '{query}'..."}) + await self.stream_output(f"🔎 Running research for '{query}'...") responses = await self.async_search(query) result = "\n".join(responses) - os.makedirs(os.path.dirname(f"./outputs/{self.directory_name}/research-{query}.txt"), exist_ok=True) - write_to_file(f"./outputs/{self.directory_name}/research-{query}.txt", result) + os.makedirs(os.path.dirname(f"{self.dir_path}/research-{query}.txt"), exist_ok=True) + write_to_file(f"{self.dir_path}/research-{query}.txt", result) return result async def conduct_research(self): @@ -134,7 +137,6 @@ async def conduct_research(self): Args: None Returns: str: The research for the given question """ - self.research_summary = read_txt_files(self.dir_path) if os.path.isdir(self.dir_path) else "" if not self.research_summary: @@ -143,8 +145,7 @@ async def conduct_research(self): research_result = await self.run_search_summary(query) self.research_summary += f"{research_result}\n\n" - await self.websocket.send_json( - {"type": "logs", "output": f"Total research words: {len(self.research_summary.split(' '))}"}) + await self.stream_output(f"Total research words: {len(self.research_summary.split(' '))}") return self.research_summary @@ -156,21 +157,23 @@ async def create_concepts(self): """ result = self.call_agent(prompts.generate_concepts_prompt(self.question, self.research_summary)) - await self.websocket.send_json({"type": "logs", "output": f"I will research based on the following concepts: {result}\n"}) + await self.stream_output(f"I will research based on the following concepts: {result}\n") return json.loads(result) - async def write_report(self, report_type, websocket): + async def write_report(self, report_type, websocket=None): """ Writes the report for the given question. Args: None Returns: str: The report for the given question """ report_type_func = prompts.get_report_by_type(report_type) - await websocket.send_json( - {"type": "logs", "output": f"✍️ Writing {report_type} for research task: {self.question}..."}) - answer = await self.call_agent(report_type_func(self.question, self.research_summary), stream=True, - websocket=websocket) + await self.stream_output(f"✍️ Writing {report_type} for research task: {self.question}...") + + answer = await self.call_agent(report_type_func(self.question, self.research_summary), + stream=websocket is not None, websocket=websocket) + # if websocket is True than we are streaming gpt response, so we need to wait for the final response + final_report = await answer if websocket else answer - path = await write_md_to_pdf(report_type, self.directory_name, await answer) + path = await write_md_to_pdf(report_type, self.dir_path, final_report) return answer, path @@ -182,4 +185,4 @@ async def write_lessons(self): concepts = await self.create_concepts() for concept in concepts: answer = await self.call_agent(prompts.generate_lesson_prompt(concept), stream=True) - write_md_to_pdf("Lesson", self.directory_name, answer) + await write_md_to_pdf("Lesson", self.dir_path, answer) diff --git a/config/config.py b/config/config.py index 865b91396..a302d6e61 100644 --- a/config/config.py +++ b/config/config.py @@ -24,9 +24,10 @@ def __init__(self) -> None: self.llm_provider = os.getenv("LLM_PROVIDER", "ChatOpenAI") self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo-16k") self.smart_llm_model = os.getenv("SMART_LLM_MODEL", "gpt-4") - self.fast_token_limit = int(os.getenv("FAST_TOKEN_LIMIT", 4000)) - self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 8000)) + self.fast_token_limit = int(os.getenv("FAST_TOKEN_LIMIT", 2000)) + self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 4000)) self.browse_chunk_max_length = int(os.getenv("BROWSE_CHUNK_MAX_LENGTH", 8192)) + self.summary_token_limit = int(os.getenv("SUMMARY_TOKEN_LIMIT", 700)) self.openai_api_key = os.getenv("OPENAI_API_KEY") self.temperature = float(os.getenv("TEMPERATURE", "1")) diff --git a/permchain_example/README.md b/permchain_example/README.md new file mode 100644 index 000000000..f8368ce03 --- /dev/null +++ b/permchain_example/README.md @@ -0,0 +1,33 @@ +# Permchain x Researcher +Sample use of Langchain's Autonomous agent framework Permchain with GPT Researcher. + +## Use case +Permchain is a framework for building autonomous agents that can be used to automate tasks and communication between agents to complete complex tasks. This example uses Permchain to automate the process of finding and summarizing research reports on any given topic. + +## The Agent Team +The research team is made up of 3 agents: +- Researcher agent (gpt-researcher) - This agent is in charge of finding and summarizing relevant research papers. +- Editor agent - This agent is in charge of validating the correctness of the report given a set of criteria. +- Reviser agent - This agent is in charge of revising the report until it is satisfactory. + +## How it works +The research agent (gpt-researcher) is in charge of finding and summarizing relevant research papers. It does this by using the following process: +- Search for relevant research papers using a search engine +- Extract the relevant information from the research papers +- Summarize the information into a report +- Send the report to the editor agent for validation +- Send the report to the reviser agent for revision +- Repeat until the report is satisfactory + +## How to run +1. Install required packages: + ```bash + pip install -r requirements.txt + ``` +2. Run the application: + ```bash + python test.py + ``` + +## Usage +To change the research topic, edit the `query` variable in `test.py` to the desired topic. \ No newline at end of file diff --git a/permchain_example/editor_actors/editor.py b/permchain_example/editor_actors/editor.py new file mode 100644 index 000000000..1c62f4fc5 --- /dev/null +++ b/permchain_example/editor_actors/editor.py @@ -0,0 +1,51 @@ +from langchain.chat_models import ChatOpenAI +from langchain.prompts import SystemMessagePromptTemplate +from config import Config + +CFG = Config() + +EDIT_TEMPLATE = """You are an editor. \ +You have been tasked with editing the following draft, which was written by a non-expert. \ +Please accept the draft if it is good enough to publish, or send it for revision, along with your notes to guide the revision. \ +Things you should be checking for: + +- This draft MUST fully answer the original question +- This draft MUST be written in apa format + +If not all of the above criteria are met, you should send appropriate revision notes. +""" + + +class EditorActor: + def __init__(self): + self.model = ChatOpenAI(model=CFG.smart_llm_model) + self.prompt = SystemMessagePromptTemplate.from_template(EDIT_TEMPLATE) + "Draft:\n\n{draft}" + self.functions = [ + { + "name": "revise", + "description": "Sends the draft for revision", + "parameters": { + "type": "object", + "properties": { + "notes": { + "type": "string", + "description": "The editor's notes to guide the revision.", + }, + }, + }, + }, + { + "name": "accept", + "description": "Accepts the draft", + "parameters": { + "type": "object", + "properties": {"ready": {"const": True}}, + }, + }, + ] + + @property + def runnable(self): + return ( + self.prompt | self.model.bind(functions=self.functions) + ) diff --git a/permchain_example/research_team.py b/permchain_example/research_team.py new file mode 100644 index 000000000..26264d209 --- /dev/null +++ b/permchain_example/research_team.py @@ -0,0 +1,73 @@ +from operator import itemgetter +from langchain.runnables.openai_functions import OpenAIFunctionsRouter + +from permchain.connection_inmemory import InMemoryPubSubConnection +from permchain.pubsub import PubSub +from permchain.topic import Topic + +''' + This is the research team. + It is a group of autonomous agents that work together to answer a given question + using a comprehensive research process that includes: + - Searching for relevant information across multiple sources + - Extracting relevant information + - Writing a well structured report + - Validating the report + - Revising the report + - Repeat until the report is satisfactory +''' +class ResearchTeam: + def __init__(self, research_actor, editor_actor, reviser_actor): + self.research_actor_instance = research_actor + self.editor_actor_instance = editor_actor + self.revise_actor_instance = reviser_actor + + def run(self, query): + # create topics + editor_inbox = Topic("editor_inbox") + reviser_inbox = Topic("reviser_inbox") + + research_chain = ( + # Listed in inputs + Topic.IN.subscribe() + | {"draft": lambda x: self.research_actor_instance.run(x["question"])} + # The draft always goes to the editor inbox + | editor_inbox.publish() + ) + + editor_chain = ( + # Listen for events in the editor_inbox + editor_inbox.subscribe() + | self.editor_actor_instance.runnable + # Depending on the output, different things should happen + | OpenAIFunctionsRouter({ + # If revise is chosen, we send a push to the critique_inbox + "revise": ( + { + "notes": itemgetter("notes"), + "draft": editor_inbox.current() | itemgetter("draft"), + "question": Topic.IN.current() | itemgetter("question"), + } + | reviser_inbox.publish() + ), + # If accepted, then we return + "accept": editor_inbox.current() | Topic.OUT.publish(), + }) + ) + + reviser_chain = ( + # Listen for events in the reviser's inbox + reviser_inbox.subscribe() + | self.revise_actor_instance.runnable + # Publish to the editor inbox + | editor_inbox.publish() + ) + + web_researcher = PubSub( + processes=(research_chain, editor_chain, reviser_chain), + connection=InMemoryPubSubConnection(), + ) + + res = web_researcher.invoke({"question": query}) + print(res) + return res[0]["draft"] diff --git a/permchain_example/researcher.py b/permchain_example/researcher.py new file mode 100644 index 000000000..48fc7b1a4 --- /dev/null +++ b/permchain_example/researcher.py @@ -0,0 +1,35 @@ +from permchain.connection_inmemory import InMemoryPubSubConnection +from permchain.pubsub import PubSub +from permchain.topic import Topic + + +class Researcher: + def __init__(self, search_actor, writer_actor): + self.search_actor_instance = search_actor + self.writer_actor_instance = writer_actor + + def run(self, query): + # The research inbox + research_inbox = Topic("research") + search_actor = ( + Topic.IN.subscribe() + | { + "query": lambda x: x, + "results": self.search_actor_instance.runnable + } + | research_inbox.publish() + ) + + write_actor = ( + research_inbox.subscribe() + | self.writer_actor_instance.runnable + | Topic.OUT.publish() + ) + + researcher = PubSub( + processes=(search_actor, write_actor), + connection=InMemoryPubSubConnection(), + ) + + res = researcher.invoke(query) + return res[0]['answer'] diff --git a/permchain_example/reviser_actors/reviser.py b/permchain_example/reviser_actors/reviser.py new file mode 100644 index 000000000..fc26134fb --- /dev/null +++ b/permchain_example/reviser_actors/reviser.py @@ -0,0 +1,24 @@ +from langchain.chat_models import ChatOpenAI, ChatAnthropic +from langchain.schema.output_parser import StrOutputParser +from langchain.prompts import SystemMessagePromptTemplate +from config import Config + +CFG = Config() + +class ReviserActor: + def __init__(self): + self.model = ChatOpenAI(model=CFG.smart_llm_model) + self.prompt = SystemMessagePromptTemplate.from_template( + "You are an expert writer. " + "You have been tasked by your editor with revising the following draft, which was written by a non-expert. " + "You may follow the editor's notes or not, as you see fit." + ) + "Draft:\n\n{draft}" + "Editor's notes:\n\n{notes}" + + @property + def runnable(self): + return { + "draft": { + "draft": lambda x: x["draft"], + "notes": lambda x: x["notes"], + } | self.prompt | self.model | StrOutputParser() + } diff --git a/permchain_example/search_actors/gpt_researcher.py b/permchain_example/search_actors/gpt_researcher.py new file mode 100644 index 000000000..899ec25fb --- /dev/null +++ b/permchain_example/search_actors/gpt_researcher.py @@ -0,0 +1,65 @@ +import json +from processing.text import summarize_text +from actions.web_scrape import scrape_text_with_selenium +from actions.web_search import web_search + +from langchain.chat_models import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain.schema.output_parser import StrOutputParser +from langchain.schema.runnable import RunnableMap, RunnableLambda +from langchain.schema.messages import SystemMessage +from agent.prompts import auto_agent_instructions, generate_search_queries_prompt +from config import Config + +CFG = Config() + +search_message = (generate_search_queries_prompt("{question}")) +SEARCH_PROMPT = ChatPromptTemplate.from_messages([ + ("system", "{agent_prompt}"), + ("user", search_message) +]) + +AUTO_AGENT_INSTRUCTIONS = auto_agent_instructions() +CHOOSE_AGENT_PROMPT = ChatPromptTemplate.from_messages([ + SystemMessage(content=AUTO_AGENT_INSTRUCTIONS), + ("user", "task: {task}") +]) + +scrape_and_summarize = { + "question": lambda x: x["question"], + "text": lambda x: scrape_text_with_selenium(x['url'])[1], + "url": lambda x: x['url'] +} | RunnableMap({ + "summary": lambda x: summarize_text(text=x["text"], question=x["question"], url=x["url"]), + "url": lambda x: x['url'] +}) | (lambda x: f"Source Url: {x['url']}\nSummary: {x['summary']}") + +seen_urls = set() +multi_search = ( + lambda x: [ + {"url": url.get("href"), "question": x["question"]} + for url in json.loads(web_search(query=x["question"], num_results=3)) + if not (url.get("href") in seen_urls or seen_urls.add(url.get("href"))) + ] +) | scrape_and_summarize.map() | (lambda x: "\n".join(x)) + +search_query = SEARCH_PROMPT | ChatOpenAI(model=CFG.smart_llm_model) | StrOutputParser() | json.loads +choose_agent = CHOOSE_AGENT_PROMPT | ChatOpenAI(model=CFG.smart_llm_model) | StrOutputParser() | json.loads + +get_search_queries = { + "question": lambda x: x, + "agent_prompt": {"task": lambda x: x} | choose_agent | (lambda x: x["agent_role_prompt"]) +} | search_query + + +class GPTResearcherActor: + + @property + def runnable(self): + return ( + get_search_queries + | (lambda x: [{"question": q} for q in x]) + | multi_search.map() + | (lambda x: "\n\n".join(x)) + ) + diff --git a/permchain_example/search_actors/search_api.py b/permchain_example/search_actors/search_api.py new file mode 100644 index 000000000..04b13c9f6 --- /dev/null +++ b/permchain_example/search_actors/search_api.py @@ -0,0 +1,13 @@ +from tavily import Client +import os +from langchain.schema.runnable import RunnableLambda + + +class TavilySearchActor: + def __init__(self): + self.api_key = os.environ["TAVILY_API_KEY"] + + @property + def runnable(self): + client = Client(self.api_key) + return RunnableLambda(client.advanced_search) | {"results": lambda x: x["results"]} diff --git a/permchain_example/test.py b/permchain_example/test.py new file mode 100644 index 000000000..736a0fd2a --- /dev/null +++ b/permchain_example/test.py @@ -0,0 +1,32 @@ +# main +import os, sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from permchain_example.researcher import Researcher +from permchain_example.search_actors.search_api import TavilySearchActor +from permchain_example.editor_actors.editor import EditorActor +from permchain_example.reviser_actors.reviser import ReviserActor +from permchain_example.search_actors.gpt_researcher import GPTResearcherActor +from permchain_example.writer_actors.writer import WriterActor +from permchain_example.research_team import ResearchTeam +from processing.text import md_to_pdf + + + +if __name__ == '__main__': + output_path = "./output" + if not os.path.exists(output_path): + # If the directory does not exist, create it + os.makedirs(output_path) + + stocks = ["NVDA"] + + for stock in stocks[:1]: + query = f"is the stock {stock} a good buy?" + researcher = Researcher(GPTResearcherActor(), WriterActor()) + research_team = ResearchTeam(researcher, EditorActor(), ReviserActor()) + + draft = research_team.run(query) + with open(f"{output_path}/{stock}.md", "w") as f: + f.write(draft) + md_to_pdf(f"{output_path}/{stock}.md", f"{output_path}/{stock}.pdf") \ No newline at end of file diff --git a/permchain_example/writer_actors/writer.py b/permchain_example/writer_actors/writer.py new file mode 100644 index 000000000..e65421b20 --- /dev/null +++ b/permchain_example/writer_actors/writer.py @@ -0,0 +1,24 @@ +from langchain.prompts import ChatPromptTemplate +from langchain.chat_models import ChatOpenAI +from langchain.schema.output_parser import StrOutputParser +from agent.prompts import generate_report_prompt, generate_agent_role_prompt +from config import Config + +CFG = Config() + +class WriterActor: + def __init__(self): + self.model = ChatOpenAI(model=CFG.smart_llm_model) + self.prompt = ChatPromptTemplate.from_messages([ + ("system", generate_agent_role_prompt(agent="Default Agent")), + ("user", generate_report_prompt(question="{query}", research_summary="{results}")) + ]) + + @property + def runnable(self): + return { + "answer": { + "query": lambda x: x["query"], + "results": lambda x: "\n\n".join(x["results"]) + } | self.prompt | self.model | StrOutputParser() + } diff --git a/processing/text.py b/processing/text.py index e3881d0f0..23edef4f1 100644 --- a/processing/text.py +++ b/processing/text.py @@ -64,11 +64,12 @@ def summarize_text( chunks = list(split_text(text)) scroll_ratio = 1 / len(chunks) + print(f"Summarizing url: {url} with total chunks: {len(chunks)}") for i, chunk in enumerate(chunks): if driver: scroll_to_percentage(driver, scroll_ratio * i) - memory_to_add = f"Source: {url}\n" f"Raw content part#{i + 1}: {chunk}" + #memory_to_add = f"Source: {url}\n" f"Raw content part#{i + 1}: {chunk}" #MEMORY.add_documents([Document(page_content=memory_to_add)]) @@ -77,20 +78,25 @@ def summarize_text( summary = create_chat_completion( model=CFG.fast_llm_model, messages=messages, + max_tokens=CFG.summary_token_limit ) summaries.append(summary) - memory_to_add = f"Source: {url}\n" f"Content summary part#{i + 1}: {summary}" + #memory_to_add = f"Source: {url}\n" f"Content summary part#{i + 1}: {summary}" #MEMORY.add_documents([Document(page_content=memory_to_add)]) - combined_summary = "\n".join(summaries) messages = [create_message(combined_summary, question)] - return create_chat_completion( + final_summary = create_chat_completion( model=CFG.fast_llm_model, messages=messages, + max_tokens=CFG.summary_token_limit ) + print("Final summary length: ", len(combined_summary)) + print(final_summary) + + return final_summary def scroll_to_percentage(driver: WebDriver, ratio: float) -> None: @@ -120,9 +126,9 @@ def create_message(chunk: str, question: str) -> Dict[str, str]: """ return { "role": "user", - "content": f'"""{chunk}""" Using the above text, answer the following' + "content": f'"""{chunk}""" Using the above text, answer in short the following' f' question: "{question}" -- if the question cannot be answered using the text,' - " simply summarize the text in depth. " + " simply summarize the text. " "Include all factual information, numbers, stats etc if available.", } @@ -136,8 +142,8 @@ def write_to_file(filename: str, text: str) -> None: with open(filename, "w") as file: file.write(text) -async def write_md_to_pdf(task: str, directory_name: str, text: str) -> None: - file_path = f"./outputs/{directory_name}/{task}" +async def write_md_to_pdf(task: str, path: str, text: str) -> None: + file_path = f"{path}/{task}" write_to_file(f"{file_path}.md", text) md_to_pdf(f"{file_path}.md", f"{file_path}.pdf") print(f"{task} written to {file_path}.pdf") diff --git a/requirements.txt b/requirements.txt index a36e022cf..521222a33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ python-multipart markdown langchain==0.0.275 tavily-python +permchain