DannyAI commited on
Commit
e6583bf
·
verified ·
1 Parent(s): 0892506

Upload 34 files

Browse files
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ HUGGINGFACEHUB_API_TOKEN=your_token_here
2
+ LANGSMITH_API_KEY=your_langsmith_key
3
+ LANGSMITH_PROJECT=your_project_name
4
+ LANGSMITH_TRACING=True
5
+ LANGSMITH_ENDPOINT="https://api.langsmith.com"
6
+ # OPENAI_API_KEY="<your-openai-api-key>"
.gitattributes CHANGED
@@ -1,35 +1,38 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ img/Agentic[[:space:]]Research[[:space:]]Assistant[[:space:]]AI.png filter=lfs diff=lfs merge=lfs -text
37
+ img/LangSmith_run.png filter=lfs diff=lfs merge=lfs -text
38
+ img/streamlit_web_page_summariser.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,14 +1,47 @@
1
- ---
2
- title: Agentic Researcher App
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: Using LangGraph for creating agents in research
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 🧠 Agentic Research Abstract Generator & Web Summariser-2
2
+ An agent-based system using LangGraph and HuggingFace models to generate research abstracts and summarize web content, with modular workflows, memory handling, and task-specific agents.
3
+
4
+ This builds upon the previous repo
5
+ - [Project Link](https://github.com/daniau23/agentic_researcher)
6
+ - [ReadyTensor-Publication](https://app.readytensor.ai/publications/the-agentic-research-abstract-generator-and-web-content-summariser-agent-with-langraph-gYeyu875mKsB)
7
+
8
+
9
+ ### 🚀 Features Improvements
10
+ - 📈 Evaluation metrics and logging
11
+ - 📈 Evaluation metrics and logging
12
+ - 📈 Testing features incorporated
13
+ - 🚀 Model deployment with streamlit
14
+
15
+ ![Research Assistant Flow Chart](img/Agentic-Research-Assistant-AI-pub-main-image.jpg)
16
+
17
+ ### **How the project goes**
18
+ - All files can only work after installing all dependencies in the `environment.yml` file
19
+ - The `notebooks folder` contains the jupyter notebook file for testing the project as a whole and for experimenting. `research_graph2.ipynb` contains the experimentation for abstract generation while `research_graph3.ipynb` contains the experimentation for web content summarisation.
20
+ - The `graph_article` folder contains the critic, writer and graph_article python files. The `writer.py`(writer agent) file takes the category and title needed for drafting the abstract while the `critic.py` (reviewer agent) reviews the generated abstract. The `graph_article.py` connects boths `writer.py`and `critic.py` by using LangGraph.
21
+ - The graph_web folder contains the grap_web, loader, search and summarizer python files. The `search.py` (search agent) file searches takes in the URL link for web search, while the `loader.py` (loader agent) loads the web page but limits it to appoximately 32,000 tokens to not exceed the max token limit. The summarizer agent in `summarizer.py` files, provides a concise summary for the the URL given. The graph_web.py connects all components together as one.
22
+ - The utils folder contains the `visualizer.py` file which creates the graphs of the `grah_web.py` and `graph_article.py` files when called in `main.py`. The generatd graphs are saved to the visuals folder.
23
+ - The `shared.py` file contains the shared state for the summarizer and abstract generator graphs.
24
+ - The `main.py` file calls all graphs together and prompts the user for if the would like to generate an abstract or summarise a webpage.
25
+ - The img folder contains the images used for visualisation, also `Langsmith_run.png` show the an example run when Langsmith is used for tracing the graph.
26
+ - The tests folder contains all necessary tests for the each component for proper integration of the project, making it ready for deployment.
27
+
28
+ ![LangSmith](img/LangSmith_run.png)
29
+
30
+ ### **Replicating this project and Example Usage**
31
+ Kindly refer to the GitHub link for the previous project for replication.
32
+
33
+ - **Run `pip install pytest>=8.4` and `pip install pytest-mock==3.14` after using the environment.yml from the previous repo**
34
+ ### **Example Streamlit Outputs**
35
+ ![Abstract-generator](img/streamlit_abstract_generator.png)
36
+
37
+ ![Webpage-summariser](img/streamlit_web_page_summariser.png)
38
+
39
+ **NB:**
40
+ - **You must have a huggingface api key to use the streamlit app. Kindly refer to the previous github repo to know how to generate a huggingface api key**
41
+
42
+ Here is the Publication on;
43
+ - [Edit ReadyTensor](https://app.readytensor.ai/publications/the-agentic-research-abstract-generator-and-web-content-summariser-agent-with-langraph-gYeyu875mKsB)
44
+
45
+ ### **Issues faced**:
46
+ - Integrating tests for the LLM agents
47
+ - Conflicts when deploying app on Streamlit, hence the environment.yml file was removed.
app.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from shared import ResearchState
3
+ from graph_article.graph_article import article_graph
4
+ from graph_web.graph_web import web_graph
5
+ from pydantic import ValidationError
6
+ import os
7
+
8
+ CREDITS_EXCEEDED_MSG = (
9
+ "You have exceeded your monthly included credits for Inference Providers. "
10
+ "Subscribe to PRO to get 20x more monthly included credits."
11
+ )
12
+
13
+ def generate_abstract(title, category):
14
+ if not title or not category:
15
+ return "Please enter both title and category."
16
+ try:
17
+ init_state = ResearchState(input=title, category=category)
18
+ final_state = article_graph.invoke(init_state)
19
+ if final_state.get("final_abstract"):
20
+ return final_state["final_abstract"]
21
+ else:
22
+ return "No abstract was accepted by the critic."
23
+ except Exception as e:
24
+ err_str = str(e)
25
+ if CREDITS_EXCEEDED_MSG in err_str:
26
+ return CREDITS_EXCEEDED_MSG
27
+ return f"Error: {err_str}"
28
+
29
+ def summarize_webpage(url):
30
+ if not url:
31
+ return "Please enter a URL."
32
+ try:
33
+ init_state = ResearchState(url=url)
34
+ final_state = web_graph.invoke(init_state)
35
+ return final_state.get("summary", "No summary available.")
36
+ except ValidationError as ve:
37
+ return f"Invalid URL: {ve}"
38
+ except Exception as e:
39
+ err_str = str(e)
40
+ if CREDITS_EXCEEDED_MSG in err_str:
41
+ return CREDITS_EXCEEDED_MSG
42
+ return f"Error: {err_str}"
43
+
44
+ with gr.Blocks() as demo:
45
+ gr.Markdown("# Agentic Research Abstract Generator and Web Content Summariser Agent With LangGraph")
46
+
47
+ with gr.Tab("Generate Research Abstract"):
48
+ title_input = gr.Textbox(label="Research Title", lines=1)
49
+ category_input = gr.Textbox(label="Category", lines=1)
50
+ abstract_output = gr.Textbox(label="Final Abstract", lines=10)
51
+ generate_btn = gr.Button("Generate Abstract")
52
+
53
+ generate_btn.click(
54
+ fn=generate_abstract,
55
+ inputs=[title_input, category_input],
56
+ outputs=abstract_output,
57
+ )
58
+
59
+ with gr.Tab("Summarize Webpage"):
60
+ url_input = gr.Textbox(label="URL to Summarize", lines=1)
61
+ summary_output = gr.Textbox(label="Summary", lines=10)
62
+ summarize_btn = gr.Button("Summarize")
63
+
64
+ summarize_btn.click(
65
+ fn=summarize_webpage,
66
+ inputs=[url_input],
67
+ outputs=summary_output,
68
+ )
69
+
70
+ if __name__ == "__main__":
71
+ demo.launch(pwa=True)
graph_article/critic.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from utils.retry import retry
3
+ from requests.exceptions import Timeout
4
+ from langchain.prompts import PromptTemplate
5
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
6
+ from utils.logger import setup_logger
7
+
8
+ logger = setup_logger(__name__)
9
+
10
+ HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
11
+ repo_id = "mistralai/Mistral-7B-Instruct-v0.3"
12
+
13
+ model_kwargs_critic = {
14
+ "max_new_tokens": 5,
15
+ "temperature": 0.1,
16
+ "timeout": 6000,
17
+ }
18
+
19
+ llm = HuggingFaceEndpoint(
20
+ repo_id=repo_id,
21
+ huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN,
22
+ **model_kwargs_critic
23
+ )
24
+ chat_model = ChatHuggingFace(llm=llm)
25
+
26
+ prompt = PromptTemplate.from_template(
27
+ "You are a strict research reviewer. Review the abstract:\n\n{abstract}\n\nRespond with 'ACCEPTED' or 'REJECTED'."
28
+ )
29
+ critic_chain = prompt | chat_model
30
+
31
+ @retry((Timeout,))
32
+ def critic_node(state):
33
+ logger.info(f"critic_node started reviewing abstract: '{state.abstract[:50]}...'")
34
+ try:
35
+ result = critic_chain.invoke({"abstract": state.abstract})
36
+ critique = result.content.strip().upper()
37
+ logger.info(f"critic_node critique result: {critique}")
38
+ return {
39
+ "critique": critique,
40
+ "final_abstract": state.abstract if critique == "ACCEPTED" else None
41
+ }
42
+ except Exception as e:
43
+ logger.error(f"critic_node error: {e}")
44
+ raise
graph_article/graph_article.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Article Graph
3
+
4
+ Workflow:
5
+ writer -> critic -> [ACCEPTED -> END, REJECTED -> writer]
6
+ """
7
+
8
+ from langgraph.graph import StateGraph, END
9
+ from shared import ResearchState
10
+ from .writer import writer_node
11
+ from .critic import critic_node
12
+
13
+ builder = StateGraph(ResearchState)
14
+
15
+ builder.add_node("writer", writer_node)
16
+ builder.add_node("critic", critic_node)
17
+
18
+ def should_accept(state):
19
+ return state.critique == "ACCEPTED"
20
+
21
+ builder.set_entry_point("writer")
22
+ builder.add_edge("writer", "critic")
23
+ builder.add_conditional_edges("critic", should_accept, {
24
+ True: END,
25
+ False: "writer"
26
+ })
27
+
28
+ article_graph = builder.compile()
graph_article/writer.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from utils.retry import retry
3
+ from requests.exceptions import Timeout
4
+ from langchain.prompts import PromptTemplate
5
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
6
+
7
+ from utils.logger import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+ HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
12
+ repo_id = "meta-llama/Llama-3.2-3B-Instruct"
13
+
14
+ model_kwargs_writer = {
15
+ "max_new_tokens": 200,
16
+ "max_length": 100,
17
+ "temperature": 0.8,
18
+ "timeout": 6000,
19
+ }
20
+
21
+ llm = HuggingFaceEndpoint(
22
+ repo_id=repo_id,
23
+ huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN,
24
+ **model_kwargs_writer
25
+ )
26
+ chat_model = ChatHuggingFace(llm=llm)
27
+
28
+ prompt = PromptTemplate.from_template(
29
+ "Generate an abstract for the paper titled '{input}' in the domain of {category}."
30
+ )
31
+
32
+ writer_chain = prompt | chat_model
33
+
34
+ @retry((Timeout,))
35
+ def writer_node(state):
36
+ logger.info(f"writer_node started with input: '{state.input}' and category: '{state.category}'")
37
+ try:
38
+ result = writer_chain.invoke({"input": state.input, "category": state.category})
39
+ logger.info("writer_node successfully generated abstract")
40
+ return {"abstract": result.content}
41
+ except Exception as e:
42
+ logger.error(f"writer_node error: {e}")
43
+ raise
graph_web/graph_web.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from shared import ResearchState
3
+ from .search import search_node
4
+ from .loader import load_node
5
+ from .summarizer import summarize_node
6
+
7
+ builder = StateGraph(ResearchState)
8
+ builder.add_node("search", search_node)
9
+ builder.add_node("load", load_node)
10
+ builder.add_node("summarize", summarize_node)
11
+
12
+ builder.set_entry_point("search")
13
+ builder.add_edge("search", "load")
14
+ builder.add_edge("load", "summarize")
15
+ builder.add_edge("summarize", END)
16
+
17
+ web_graph = builder.compile()
graph_web/loader.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_community.document_loaders import SeleniumURLLoader
2
+ from shared import ResearchState
3
+
4
+ # Limit content length to ~100,000 characters (≈ 32,000 tokens max)
5
+ MAX_CHARS = 100_000
6
+
7
+ def load_node(state: ResearchState) -> dict:
8
+ if not state.url:
9
+ return {"content": "No URL to load"}
10
+
11
+ loader = SeleniumURLLoader(urls=[str(state.url)])
12
+ docs = loader.load()
13
+
14
+ content = docs[0].page_content if docs else "No content"
15
+
16
+ # Truncate early to prevent overload later
17
+ truncated_content = content[:MAX_CHARS]
18
+ return {"content": truncated_content}
graph_web/search.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shared import ResearchState
2
+ from pydantic import ValidationError
3
+
4
+ DEFAULT_URL = "https://www.mdpi.com/2076-3417/11/20/9772"
5
+
6
+ def search_node(state: ResearchState) -> dict:
7
+ if state.url:
8
+ return {"url": str(state.url)}
9
+
10
+ user_input = input(f"Enter URL to summarize [default: {DEFAULT_URL}]: ").strip()
11
+ url_input = user_input if user_input else DEFAULT_URL
12
+
13
+ try:
14
+ validated = ResearchState(url=url_input)
15
+ except ValidationError as e:
16
+ raise ValueError(f"Invalid URL: {e}")
17
+
18
+ return {"url": str(validated.url)}
graph_web/summarizer.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from utils.retry import retry
3
+ from requests.exceptions import Timeout
4
+ from langchain.prompts import PromptTemplate
5
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
6
+
7
+ from utils.logger import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+ HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
12
+ repo_id = "mistralai/Mistral-7B-Instruct-v0.3"
13
+
14
+ model_kwargs = {
15
+ "temperature": 0.1,
16
+ "max_new_tokens": 100,
17
+ "timeout": 6000,
18
+ }
19
+
20
+ llm = HuggingFaceEndpoint(
21
+ repo_id=repo_id,
22
+ huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN,
23
+ **model_kwargs
24
+ )
25
+ chat_model = ChatHuggingFace(llm=llm)
26
+
27
+ prompt = PromptTemplate.from_template(
28
+ "Summarize the following content concisely:\n\n{content}\n\nSummary:"
29
+ )
30
+ summarize_chain = prompt | chat_model
31
+
32
+ @retry((Timeout,))
33
+ def summarize_node(state):
34
+ logger.info("summarize_node started")
35
+ if not state.content:
36
+ logger.warning("summarize_node found no content to summarize")
37
+ return {"summary": "No content to summarize"}
38
+ try:
39
+ result = summarize_chain.invoke({"content": state.content})
40
+ logger.info("summarize_node successfully generated summary")
41
+ return {"summary": result.content}
42
+ except Exception as e:
43
+ logger.error(f"summarize_node error: {e}")
44
+ raise
img/Agentic Research Assistant AI.drawio ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0" version="28.0.6" pages="2">
2
+ <diagram name="flow image" id="FoGwzNI2L2qQ2lyo0fvc">
3
+ <mxGraphModel dx="2647" dy="2599" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
4
+ <root>
5
+ <mxCell id="0" />
6
+ <mxCell id="1" parent="0" />
7
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DKvXnGrd76LMYUC1vWfy-3" edge="1">
8
+ <mxGeometry relative="1" as="geometry">
9
+ <mxPoint x="-300" y="160" as="targetPoint" />
10
+ </mxGeometry>
11
+ </mxCell>
12
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DKvXnGrd76LMYUC1vWfy-3" edge="1">
13
+ <mxGeometry relative="1" as="geometry">
14
+ <mxPoint x="320" y="150" as="targetPoint" />
15
+ </mxGeometry>
16
+ </mxCell>
17
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="DKvXnGrd76LMYUC1vWfy-3" edge="1">
18
+ <mxGeometry relative="1" as="geometry">
19
+ <mxPoint x="660" y="80" as="targetPoint" />
20
+ </mxGeometry>
21
+ </mxCell>
22
+ <mxCell id="DKvXnGrd76LMYUC1vWfy-3" value="&lt;font&gt;Manager Node&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" parent="1" vertex="1">
23
+ <mxGeometry x="-80" y="-150" width="160" height="100" as="geometry" />
24
+ </mxCell>
25
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-3" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/jpeg,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;" parent="1" vertex="1">
26
+ <mxGeometry x="-340" y="160" width="210" height="731.62" as="geometry" />
27
+ </mxCell>
28
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-4" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/jpeg,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;" parent="1" vertex="1">
29
+ <mxGeometry x="240" y="150" width="218.17" height="776.82" as="geometry" />
30
+ </mxCell>
31
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-5" value="" style="curved=1;endArrow=classic;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.398;entryY=1.02;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="fbFRsy6pGwT7WNmv-kii-3" target="DKvXnGrd76LMYUC1vWfy-3" edge="1">
32
+ <mxGeometry width="50" height="50" relative="1" as="geometry">
33
+ <mxPoint x="-40" y="730" as="sourcePoint" />
34
+ <mxPoint x="10" y="680" as="targetPoint" />
35
+ <Array as="points">
36
+ <mxPoint x="10" y="730" />
37
+ <mxPoint x="-40" y="680" />
38
+ </Array>
39
+ </mxGeometry>
40
+ </mxCell>
41
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-6" value="" style="curved=1;endArrow=classic;html=1;rounded=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="fbFRsy6pGwT7WNmv-kii-4" target="DKvXnGrd76LMYUC1vWfy-3" edge="1">
42
+ <mxGeometry width="50" height="50" relative="1" as="geometry">
43
+ <mxPoint x="80" y="1200" as="sourcePoint" />
44
+ <mxPoint x="194" y="260" as="targetPoint" />
45
+ <Array as="points">
46
+ <mxPoint x="150" y="860" />
47
+ <mxPoint x="170" y="988" />
48
+ </Array>
49
+ </mxGeometry>
50
+ </mxCell>
51
+ <mxCell id="fbFRsy6pGwT7WNmv-kii-8" value="&lt;font&gt;exit&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" parent="1" vertex="1">
52
+ <mxGeometry x="590" y="80" width="160" height="100" as="geometry" />
53
+ </mxCell>
54
+ </root>
55
+ </mxGraphModel>
56
+ </diagram>
57
+ <diagram name="pub main image" id="1uLcNRWAyeX4VGRSqUz4">
58
+ <mxGraphModel dx="1555" dy="1741" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
59
+ <root>
60
+ <mxCell id="I85ioxBOdSItPDKS9CG3-0" />
61
+ <mxCell id="I85ioxBOdSItPDKS9CG3-1" parent="I85ioxBOdSItPDKS9CG3-0" />
62
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;curved=1;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-21" target="S1dxI5CfeVDpxhQQVvJD-27">
63
+ <mxGeometry relative="1" as="geometry" />
64
+ </mxCell>
65
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;curved=1;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-21" target="S1dxI5CfeVDpxhQQVvJD-31">
66
+ <mxGeometry relative="1" as="geometry" />
67
+ </mxCell>
68
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-42" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-21" target="S1dxI5CfeVDpxhQQVvJD-41">
69
+ <mxGeometry relative="1" as="geometry" />
70
+ </mxCell>
71
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-21" value="&lt;font&gt;AI AGENT&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
72
+ <mxGeometry x="-450" y="20" width="281" height="100" as="geometry" />
73
+ </mxCell>
74
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-36" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;curved=1;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-27" target="S1dxI5CfeVDpxhQQVvJD-35">
75
+ <mxGeometry relative="1" as="geometry" />
76
+ </mxCell>
77
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-45" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;curved=0;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-27" target="S1dxI5CfeVDpxhQQVvJD-43">
78
+ <mxGeometry relative="1" as="geometry" />
79
+ </mxCell>
80
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-27" value="&lt;b&gt;ABSTRACT GENRATOR&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
81
+ <mxGeometry x="110" y="240" width="290" height="100" as="geometry" />
82
+ </mxCell>
83
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-49" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;curved=0;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-31" target="S1dxI5CfeVDpxhQQVvJD-48">
84
+ <mxGeometry relative="1" as="geometry" />
85
+ </mxCell>
86
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-52" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;curved=0;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-31" target="S1dxI5CfeVDpxhQQVvJD-46">
87
+ <mxGeometry relative="1" as="geometry" />
88
+ </mxCell>
89
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-31" value="&lt;b&gt;WEB CONTENT SUMMARISER&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
90
+ <mxGeometry x="-46" y="-180" width="290" height="100" as="geometry" />
91
+ </mxCell>
92
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;curved=1;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-35" target="S1dxI5CfeVDpxhQQVvJD-37">
93
+ <mxGeometry relative="1" as="geometry" />
94
+ </mxCell>
95
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-51" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;curved=0;strokeWidth=8;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-35" target="S1dxI5CfeVDpxhQQVvJD-27">
96
+ <mxGeometry relative="1" as="geometry">
97
+ <Array as="points">
98
+ <mxPoint x="-30" y="510" />
99
+ <mxPoint x="-30" y="290" />
100
+ </Array>
101
+ </mxGeometry>
102
+ </mxCell>
103
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-35" value="&lt;b&gt;WRITER&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;fontColor=#ffffff;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
104
+ <mxGeometry x="244" y="460" width="290" height="100" as="geometry" />
105
+ </mxCell>
106
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;curved=1;dashed=1;dashPattern=8 8;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-37" target="S1dxI5CfeVDpxhQQVvJD-35">
107
+ <mxGeometry relative="1" as="geometry" />
108
+ </mxCell>
109
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-37" value="&lt;b&gt;CRITIC&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;fontColor=#ffffff;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
110
+ <mxGeometry x="560" y="670" width="290" height="100" as="geometry" />
111
+ </mxCell>
112
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-41" value="&lt;font&gt;&lt;b&gt;EXIT&lt;/b&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B30000;strokeColor=#B30000;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
113
+ <mxGeometry x="200" y="20" width="270" height="100" as="geometry" />
114
+ </mxCell>
115
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-43" value="&lt;b&gt;&lt;font&gt;GENERATED ABSTRACT&lt;/font&gt;&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C9E74C;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
116
+ <mxGeometry x="550" y="240" width="290" height="100" as="geometry" />
117
+ </mxCell>
118
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-46" value="&lt;b&gt;&lt;font&gt;SUMMARISE CONTENT&lt;/font&gt;&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C9E74C;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
119
+ <mxGeometry x="550" y="-180" width="290" height="100" as="geometry" />
120
+ </mxCell>
121
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-50" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;strokeWidth=8;" edge="1" parent="I85ioxBOdSItPDKS9CG3-1" source="S1dxI5CfeVDpxhQQVvJD-48" target="S1dxI5CfeVDpxhQQVvJD-31">
122
+ <mxGeometry relative="1" as="geometry" />
123
+ </mxCell>
124
+ <mxCell id="S1dxI5CfeVDpxhQQVvJD-48" value="&lt;b&gt;SUMMARISER&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;sketch=1;curveFitting=1;jiggle=2;shadow=1;glass=0;fontFamily=Times New Roman;fontSize=30;fontColor=#ffffff;" vertex="1" parent="I85ioxBOdSItPDKS9CG3-1">
125
+ <mxGeometry x="-46" y="-430" width="290" height="100" as="geometry" />
126
+ </mxCell>
127
+ </root>
128
+ </mxGraphModel>
129
+ </diagram>
130
+ </mxfile>
img/Agentic Research Assistant AI.png ADDED

Git LFS Details

  • SHA256: f195c1575d36e77e59e246195447011d1cb4dd55b90293a5b0d813126062278b
  • Pointer size: 131 Bytes
  • Size of remote file: 274 kB
img/Agentic-Research-Assistant-AI-pub-main-image.jpg ADDED
img/LangSmith_run.png ADDED

Git LFS Details

  • SHA256: 0f20e93ea5389c4d9fcf7928aff1fcb119ab6e265c9d06c2992bfd056b3a8835
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
img/streamlit_abstract_generator.png ADDED
img/streamlit_web_page_summariser.png ADDED

Git LFS Details

  • SHA256: 130fdb923c10e44a3c7f6d385ff81eda24410782d49e505696dbf09dbbe66922
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
main.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shared import ResearchState
2
+ from graph_article.graph_article import article_graph
3
+ from graph_web.graph_web import web_graph
4
+ from dotenv import load_dotenv
5
+ from graph_web.search import DEFAULT_URL
6
+ from pydantic import ValidationError
7
+ from utils.visualizer import graph_visualiser
8
+ # Import evaluation tool
9
+ from utils.evaluation import evaluate_abstract
10
+
11
+ import os
12
+ load_dotenv('.env') # Load environment variables
13
+
14
+ LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
15
+ LANGSMITH_TRACING = os.getenv("LANGSMITH_TRACING")
16
+ LANGSMITH_PROJECT = os.getenv("LANGSMITH_PROJECT")
17
+ LANGSMITH_ENDPOINT = os.getenv("LANGSMITH_ENDPOINT")
18
+
19
+ def main():
20
+ print("=== Welcome to the LangGraph Research Assistant ===")
21
+
22
+ while True:
23
+ print("\nSelect a task to perform:")
24
+ print("1. Generate Research Abstract")
25
+ print("2. Summarize Webpages")
26
+ print("3. Exit")
27
+
28
+ choice = input("Enter your choice (1/2/3): ").strip()
29
+
30
+ if choice == "1":
31
+ title = input("Enter research title: ")
32
+ category = input("Enter category: ")
33
+ init_state = ResearchState(input=title, category=category)
34
+ final_state = article_graph.invoke(init_state)
35
+
36
+ if final_state.get("final_abstract"):
37
+ print("\n--- ✅ Final Abstract ---")
38
+ print(final_state["final_abstract"])
39
+
40
+ # 🧠 Run Evaluation
41
+ evaluation = evaluate_abstract(final_state["final_abstract"])
42
+ print("\n--- 📊 Evaluation ---")
43
+ print(f"Word Count: {evaluation['word_count']}")
44
+ print(f"Keyword Match Score: {evaluation['keyword_match_score']}")
45
+ print(f"Keywords Present: {', '.join(evaluation['keywords_present'])}")
46
+
47
+ else:
48
+ print("\n❌ No final abstract was accepted by the critic.")
49
+
50
+ elif choice == "2":
51
+ print("\n📄 Web Summarizer Mode (Press Enter to exit anytime)")
52
+ while True:
53
+ user_input = input(f"\nEnter a URL to summarize [default: {DEFAULT_URL}]: ").strip()
54
+ if not user_input:
55
+ print("🔚 Returning to main menu...")
56
+ break
57
+
58
+ try:
59
+ init_state = ResearchState(url=user_input)
60
+ final_state = web_graph.invoke(init_state)
61
+ print("\n--- Webpage Summary ---")
62
+ print(final_state["summary"])
63
+ except ValidationError as e:
64
+ print(f"❌ Invalid URL: {e}")
65
+
66
+ elif choice == "3":
67
+ print("👋 Goodbye!")
68
+ break
69
+
70
+ else:
71
+ print("❌ Invalid input. Try again.")
72
+
73
+ if __name__ == "__main__":
74
+ # Optional: visualize LangGraphs
75
+ graph_visualiser(web_graph, filename="visuals/web_graph.jpg")
76
+ graph_visualiser(article_graph, filename="visuals/article_graph.jpg")
77
+
78
+ main()
notebooks/research_graph2.ipynb ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from typing import Annotated, List, Literal, Optional\n",
10
+ "from pydantic import BaseModel, Field\n",
11
+ "from operator import add\n",
12
+ "from langgraph.graph.message import add_messages\n",
13
+ "from langgraph.graph import StateGraph, END\n",
14
+ "from langgraph.graph.state import CompiledStateGraph\n",
15
+ "from IPython.display import Image, display\n",
16
+ "# HuggingFace\n",
17
+ "from langchain_huggingface.embeddings import HuggingFaceEndpointEmbeddings\n",
18
+ "from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint\n",
19
+ "# Prompts\n",
20
+ "from langchain.prompts import PromptTemplate\n",
21
+ "from langchain_core.messages import BaseMessage\n",
22
+ "from langchain_core.tools import tool\n",
23
+ "# Others\n",
24
+ "import os\n",
25
+ "from dotenv import load_dotenv\n",
26
+ "# # LangSmith\n",
27
+ "# import uuid\n",
28
+ "# from langchain_core.tracers.context import collect_runs\n",
29
+ "# from langchain_core.tracers.langchain import wait_for_all_tracers\n",
30
+ "# from langsmith import Client\n",
31
+ "# from langchain.callbacks.tracers import LangChainTracer"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": 2,
37
+ "id": "ab89c1bc",
38
+ "metadata": {},
39
+ "outputs": [],
40
+ "source": [
41
+ "# Understanding Pydantic and Typing\n",
42
+ "# https://typing.python.org/en/latest/spec/annotations.html\n",
43
+ "# https://docs.pydantic.dev/latest/\n",
44
+ "# https://medium.com/@moraneus/exploring-the-power-of-pythons-typing-library-ff32cec44981\n",
45
+ "# https://coderivers.org/blog/typingannotated-python/\n",
46
+ "# https://chatgpt.com/share/68644926-ccfc-800a-b668-4752077e3a29"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "code",
51
+ "execution_count": 3,
52
+ "id": "d137f5ad",
53
+ "metadata": {},
54
+ "outputs": [
55
+ {
56
+ "data": {
57
+ "text/plain": [
58
+ "True"
59
+ ]
60
+ },
61
+ "execution_count": 3,
62
+ "metadata": {},
63
+ "output_type": "execute_result"
64
+ }
65
+ ],
66
+ "source": [
67
+ "# Provide the filename as a string\n",
68
+ "load_dotenv('../.env')"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "id": "68a6a30e",
74
+ "metadata": {},
75
+ "source": [
76
+ "## **Define State**"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "code",
81
+ "execution_count": 4,
82
+ "id": "bfe2ebdb",
83
+ "metadata": {},
84
+ "outputs": [],
85
+ "source": [
86
+ "class ResearchArticle(BaseModel):\n",
87
+ " text: str\n",
88
+ " title:str\n",
89
+ " category: str\n",
90
+ "\n",
91
+ "class ResearchArticleState(BaseModel):\n",
92
+ " \"\"\"\n",
93
+ " Represents the evolving state of the article bot.\n",
94
+ " \"\"\"\n",
95
+ " article: Annotated[List[ResearchArticle], add] = []\n",
96
+ " articles_choice: Literal[\"n\", \"c\", \"q\"] = \"n\"\n",
97
+ " category: str = \"nlp\"\n",
98
+ " title:str = \"Solving the myth and facts of child development with nlp\"\n",
99
+ " quit: bool = False"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": 5,
105
+ "id": "ae02f563",
106
+ "metadata": {},
107
+ "outputs": [],
108
+ "source": [
109
+ "class AgenticResearchArticleState(ResearchArticleState):\n",
110
+ " latest_article: str = \"\"\n",
111
+ " approved: bool = False\n",
112
+ " retry_count:int = 0"
113
+ ]
114
+ },
115
+ {
116
+ "cell_type": "markdown",
117
+ "id": "8dffeb22",
118
+ "metadata": {},
119
+ "source": [
120
+ "## **Utilities**"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "code",
125
+ "execution_count": 6,
126
+ "id": "1cc7442a",
127
+ "metadata": {},
128
+ "outputs": [],
129
+ "source": [
130
+ "def get_user_input(prompt:str)-> str:\n",
131
+ " prompt_input = input(prompt)\n",
132
+ " prompt_input = \"\".join(prompt_input).strip().lower()\n",
133
+ " return prompt_input\n",
134
+ "\n",
135
+ "def print_article(article:ResearchArticle):\n",
136
+ " \"\"\"\n",
137
+ " Print article with nice formatting\n",
138
+ " \"\"\"\n",
139
+ " print(f\"\\nCATEGORY: {article.category.upper()}\\n\")\n",
140
+ " print(f\"\\n{article.title}\\n\")\n",
141
+ " print(f\"\\n{article.text}\\n\")\n",
142
+ " print(\"=\"* 60)\n",
143
+ "\n",
144
+ "def print_menu_header(category:str, total_articles:int):\n",
145
+ " \"\"\"\n",
146
+ " Print a compact menu header\n",
147
+ " \"\"\"\n",
148
+ " print(f\"🎭 Menu | Category: {category.upper()} | Articles: {total_articles}\")\n",
149
+ " print(\"-\" * 50)"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "markdown",
154
+ "id": "699940c6",
155
+ "metadata": {},
156
+ "source": [
157
+ "## **LLMS**"
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "code",
162
+ "execution_count": 7,
163
+ "id": "3edd2c97",
164
+ "metadata": {},
165
+ "outputs": [
166
+ {
167
+ "name": "stderr",
168
+ "output_type": "stream",
169
+ "text": [
170
+ "WARNING! max_length is not default parameter.\n",
171
+ " max_length was transferred to model_kwargs.\n",
172
+ " Please make sure that max_length is what you intended.\n"
173
+ ]
174
+ }
175
+ ],
176
+ "source": [
177
+ "# Provide the filename as a string\n",
178
+ "load_dotenv('.env')\n",
179
+ "\n",
180
+ "\n",
181
+ "# loadding Huggingface token\n",
182
+ "HUGGINGFACEHUB_API_TOKEN = os.getenv(\"HUGGINGFACEHUB_API_TOKEN\")\n",
183
+ "\n",
184
+ "# models \n",
185
+ "repo_id_writer = \"mistralai/Mistral-7B-Instruct-v0.3\"\n",
186
+ "repo_id_critic = \"mistralai/Mistral-7B-Instruct-v0.3\"\n",
187
+ "# repo_id_critic = \"mistralai/Mistral-Small-24B-Instruct-2501\"\n",
188
+ "\n",
189
+ "\n",
190
+ "# model parameters\n",
191
+ "model_kwargs_writer = {\n",
192
+ " \"max_new_tokens\": 200, # Maximum tokens to generate\n",
193
+ " \"max_length\": 100, # Maximum length of input + output\n",
194
+ " \"temperature\": 0.8, # Controls randomness of output\n",
195
+ " \"timeout\": 6000,\n",
196
+ " # \"task\":'conversational'\n",
197
+ "}\n",
198
+ "\n",
199
+ "# LLM set up\n",
200
+ "llm_writer = HuggingFaceEndpoint(\n",
201
+ " repo_id=repo_id_writer,\n",
202
+ " huggingfacehub_api_token = HUGGINGFACEHUB_API_TOKEN,\n",
203
+ " **model_kwargs_writer\n",
204
+ " # you specify the task or not\n",
205
+ " # You can also specify the task in the model_kwargs or within here\n",
206
+ " # task = 'conversational',\n",
207
+ ")\n",
208
+ "# model parameters\n",
209
+ "model_kwargs_critic = {\n",
210
+ " \"max_new_tokens\": 5, # Maximum tokens to generate\n",
211
+ " # \"max_length\": 4000, # Maximum length of input + output\n",
212
+ " \"temperature\": 0.1, # Controls randomness of output\n",
213
+ " \"timeout\": 6000,\n",
214
+ " # \"task\":'conversational'\n",
215
+ "}\n",
216
+ "\n",
217
+ "# LLM set up\n",
218
+ "llm_critic = HuggingFaceEndpoint(\n",
219
+ " repo_id=repo_id_critic,\n",
220
+ " huggingfacehub_api_token = HUGGINGFACEHUB_API_TOKEN,\n",
221
+ " **model_kwargs_critic\n",
222
+ " # you specify the task or not\n",
223
+ " # You can also specify the task in the model_kwargs or within here\n",
224
+ " # task = 'conversational',\n",
225
+ ")\n",
226
+ "\n",
227
+ "chat_model_writer = ChatHuggingFace(llm=llm_writer)\n",
228
+ "chat_model_critic = ChatHuggingFace(llm=llm_critic)"
229
+ ]
230
+ },
231
+ {
232
+ "cell_type": "markdown",
233
+ "id": "863283ed",
234
+ "metadata": {},
235
+ "source": [
236
+ "## **Prompts**"
237
+ ]
238
+ },
239
+ {
240
+ "cell_type": "code",
241
+ "execution_count": 8,
242
+ "id": "cc2ebd9d",
243
+ "metadata": {},
244
+ "outputs": [],
245
+ "source": [
246
+ "# Writer prompt\n",
247
+ "WRITER_PROMPT = \"\"\"\n",
248
+ "You are the best captivating research abstract generator \n",
249
+ "with 20 years of experience \n",
250
+ "who can write the most interesting research abstracts.\\n\n",
251
+ "When given a category and title, be as detailed and concise as possible.\\n\\n\\n\n",
252
+ "Here are examples\n",
253
+ "======================================================\n",
254
+ "category: nlp\n",
255
+ "Title:The Next Frontier in Natural Language Processing\n",
256
+ "\n",
257
+ "Abstract\n",
258
+ "Natural Language Processing (NLP) has evolved from rule-based text manipulation to enabling human-like interactions across billions of devices. While transformers like BERT and GPT revolutionized text understanding, the next frontier lies in grounding language in real-world context, emotion, and dynamic memory. This article explores the state-of-the-art in NLP, its challenges, and the emerging paradigms shaping its future—from multimodal intelligence to reasoning-aware models.\n",
259
+ "======================================================\n",
260
+ "\\n\\n\\n\n",
261
+ "Category: {category}\n",
262
+ "Title: {title}\n",
263
+ "Abstract: \n",
264
+ "\"\"\"\n",
265
+ "# Critic prompt\n",
266
+ "CRITIC_PROMPT = \"\"\"\n",
267
+ "Daniel the greatest critic and editor of research articles.\\n\n",
268
+ "You evaluate all research article blog post to see if they are structured or not.\n",
269
+ "You reponse only by saying YES or NO.\\n\\n\\n\n",
270
+ "======================================================\n",
271
+ "Here are examples\n",
272
+ "category: nlp\n",
273
+ "Title: Eyes of the Machine: The Expanding Horizon of Computer Vision\n",
274
+ "Abstract:\n",
275
+ "Computer Vision, once limited to edge detection and barcode scanning, now powers autonomous vehicles, medical diagnosis, and generative art. From convolutional neural networks (CNNs) to vision-language transformers and neuromorphic sensors, the field is undergoing a seismic transformation. This article explores the evolving landscape of CV, current breakthroughs, limitations, and the emerging frontier of visual intelligence.\n",
276
+ "Response: YES\n",
277
+ "======================================================\n",
278
+ "\\n\\n\\n\n",
279
+ "Abstract: {abstract}\n",
280
+ "RESPONSE: \n",
281
+ "\"\"\""
282
+ ]
283
+ },
284
+ {
285
+ "cell_type": "code",
286
+ "execution_count": 9,
287
+ "id": "8061d5db",
288
+ "metadata": {},
289
+ "outputs": [],
290
+ "source": [
291
+ "writer_input_variable = ['category','title']\n",
292
+ "writer_prompt = PromptTemplate(template=WRITER_PROMPT,\n",
293
+ " input_variables=writer_input_variable)\n",
294
+ "\n",
295
+ "\n",
296
+ "critic_prompt_variable = ['abstract']\n",
297
+ "critic_prompt = PromptTemplate(\n",
298
+ " template=CRITIC_PROMPT,input_variables=critic_prompt_variable\n",
299
+ ")"
300
+ ]
301
+ },
302
+ {
303
+ "cell_type": "markdown",
304
+ "id": "fe73f7c2",
305
+ "metadata": {},
306
+ "source": [
307
+ "## **Define Nodes**"
308
+ ]
309
+ },
310
+ {
311
+ "cell_type": "code",
312
+ "execution_count": null,
313
+ "id": "4f2c54b0",
314
+ "metadata": {},
315
+ "outputs": [],
316
+ "source": [
317
+ "def make_writer_node(llm):\n",
318
+ " def writer_node(state: AgenticResearchArticleState) -> dict:\n",
319
+ " chain = writer_prompt | llm\n",
320
+ " response = chain.invoke({\"category\": state.category, \"title\": state.title})\n",
321
+ " content = response.content.strip().lower()\n",
322
+ " return {\"latest_article\": content}\n",
323
+ " return writer_node"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "code",
328
+ "execution_count": null,
329
+ "id": "7d09a484",
330
+ "metadata": {},
331
+ "outputs": [],
332
+ "source": [
333
+ "# critic-node\n",
334
+ "def make_critic_node(llm):\n",
335
+ " def critic_node(state:AgenticResearchArticleState)-> dict:\n",
336
+ " critic = critic_prompt | llm\n",
337
+ " decision = critic.invoke({\"abstract\": state.latest_article})\n",
338
+ " decision = decision.content.strip().lower()\n",
339
+ " approved = \"yes\" in decision\n",
340
+ " return {\n",
341
+ " \"approved\":approved,\n",
342
+ " \"retry_count\":state.retry_count + 1\n",
343
+ " }\n",
344
+ " return critic_node"
345
+ ]
346
+ },
347
+ {
348
+ "cell_type": "code",
349
+ "execution_count": 12,
350
+ "id": "70267684",
351
+ "metadata": {},
352
+ "outputs": [],
353
+ "source": [
354
+ "def show_final_article(state:AgenticResearchArticleState) -> dict:\n",
355
+ " article = ResearchArticle(\n",
356
+ " text=state.latest_article,\n",
357
+ " category=state.category,\n",
358
+ " title=state.title\n",
359
+ " )\n",
360
+ " print_article(article)\n",
361
+ " return {\n",
362
+ " \"article\":[article],\n",
363
+ " \"retry_count\": 0,\n",
364
+ " 'approved': False\n",
365
+ " }"
366
+ ]
367
+ },
368
+ {
369
+ "cell_type": "code",
370
+ "execution_count": 13,
371
+ "id": "91ec73b8",
372
+ "metadata": {},
373
+ "outputs": [],
374
+ "source": [
375
+ "def writer_critic_router(state: AgenticResearchArticleState) -> str:\n",
376
+ " if state.approved or state.retry_count >= 20:\n",
377
+ " return \"show_final_article\"\n",
378
+ " return \"writer\""
379
+ ]
380
+ },
381
+ {
382
+ "cell_type": "code",
383
+ "execution_count": 14,
384
+ "id": "83f15417",
385
+ "metadata": {},
386
+ "outputs": [],
387
+ "source": [
388
+ "def show_menu(state: ResearchArticleState) -> dict:\n",
389
+ " print_menu_header(state.category, len(state.article))\n",
390
+ " print(\"Pick an option\")\n",
391
+ " user_input = get_user_input(\n",
392
+ " \"[n] 🎭 Next Article \\n[c] 📂 Change Category\\n[q] 🚪 Quit\\nUser Input: \"\n",
393
+ " )\n",
394
+ "\n",
395
+ " while user_input not in [\"n\", \"c\", \"q\"]:\n",
396
+ " print(\"❌ Invalid input. Please try again.\")\n",
397
+ " user_input = get_user_input(\n",
398
+ " \"[n] 🎭 Next Article [c] 📂 Change Category [q] 🚪 Quit\\n User Input: \"\n",
399
+ " )\n",
400
+ "\n",
401
+ " result = {\"articles_choice\": user_input}\n",
402
+ "\n",
403
+ " # Always prompt for title when 'n' is chosen\n",
404
+ " if user_input == \"n\":\n",
405
+ " title = input(\"\\nEnter a title for the new article:\\n\\nTITLE: \")\n",
406
+ " result[\"title\"] = title.strip()\n",
407
+ "\n",
408
+ " return result\n",
409
+ "\n",
410
+ "\n",
411
+ "\n",
412
+ "# # Original\n",
413
+ "# def show_menu(state:ResearchArticleState) -> dict:\n",
414
+ "# print_menu_header(state.category,len(state.article))\n",
415
+ "# print(\"Pick an option\")\n",
416
+ "# user_input = get_user_input(\n",
417
+ "# \"[n] 🎭 Next Article \\n[c] 📂 Change Category\\n[q] 🚪 Quit\\nUser Input: \"\n",
418
+ "# )\n",
419
+ "\n",
420
+ "# while user_input not in [\"n\", \"c\", \"q\"]:\n",
421
+ "# print(\"❌ Invalid input. Please try again.\")\n",
422
+ "# user_input = get_user_input(\n",
423
+ "# \"[n] 🎭 Next Article [c] 📂 Change Category [q] 🚪 Quit\\n User Input: \"\n",
424
+ "# )\n",
425
+ "# return {\"articles_choice\": user_input}"
426
+ ]
427
+ },
428
+ {
429
+ "cell_type": "code",
430
+ "execution_count": 15,
431
+ "id": "c9c4798c",
432
+ "metadata": {},
433
+ "outputs": [],
434
+ "source": [
435
+ "def update_category(state: AgenticResearchArticleState) -> dict:\n",
436
+ " categories = [\"nlp\", \n",
437
+ " \"computer_vision\", \n",
438
+ " \"gen_ai\"]\n",
439
+ " print(\"CATEGORY SELECTION\")\n",
440
+ " print(\"=\" * 60)\n",
441
+ "\n",
442
+ " for i, cat in enumerate(categories):\n",
443
+ " print(f\" {i}. {cat.upper()}\")\n",
444
+ "\n",
445
+ " print(\"=\" * 60)\n",
446
+ "\n",
447
+ " try:\n",
448
+ " selection = int(input(\" Enter category number: \").strip())\n",
449
+ " # title = input(\"What is your title? \")\n",
450
+ " if 0 <= selection < len(categories):\n",
451
+ " selected_category = categories[selection]\n",
452
+ " print(f\" ✅ Category changed to: {selected_category.upper()}\")\n",
453
+ " return {\n",
454
+ " \"category\": selected_category,\n",
455
+ " # \"title\":title\n",
456
+ " }\n",
457
+ " else:\n",
458
+ " print(\" ❌ Invalid choice. Keeping current category.\")\n",
459
+ " return {}\n",
460
+ " except ValueError:\n",
461
+ " print(\" ❌ Please enter a valid number. Keeping current category.\")\n",
462
+ " return {}"
463
+ ]
464
+ },
465
+ {
466
+ "cell_type": "code",
467
+ "execution_count": 16,
468
+ "id": "d0b4ff1a",
469
+ "metadata": {},
470
+ "outputs": [],
471
+ "source": [
472
+ "def exit_bot(state:ResearchArticleState) -> dict:\n",
473
+ " print(\"\\n\" + \"🚪\" + \"=\" * 58 + \"🚪\")\n",
474
+ " print(\" GOODBYE!\")\n",
475
+ " print(\"=\" * 60)\n",
476
+ " return {\"quit\": True}"
477
+ ]
478
+ },
479
+ {
480
+ "cell_type": "code",
481
+ "execution_count": 17,
482
+ "id": "3c2d7b22",
483
+ "metadata": {},
484
+ "outputs": [],
485
+ "source": [
486
+ "def route_choice(state:ResearchArticleState)-> str:\n",
487
+ " \"\"\"\n",
488
+ " Router function to determine the next node based on user choice.\n",
489
+ " Keys must match the target node names.\n",
490
+ " \"\"\"\n",
491
+ " if state.articles_choice == \"n\":\n",
492
+ " return \"fetch_article\"\n",
493
+ " elif state.articles_choice == \"c\":\n",
494
+ " return \"update_category\"\n",
495
+ " elif state.articles_choice == \"q\":\n",
496
+ " return \"exit_bot\"\n",
497
+ " else:\n",
498
+ " return \"exit_bot\""
499
+ ]
500
+ },
501
+ {
502
+ "cell_type": "markdown",
503
+ "id": "0a1998d8",
504
+ "metadata": {},
505
+ "source": [
506
+ "## **Building the Graph**"
507
+ ]
508
+ },
509
+ {
510
+ "cell_type": "code",
511
+ "execution_count": 18,
512
+ "id": "74033c0f",
513
+ "metadata": {},
514
+ "outputs": [],
515
+ "source": [
516
+ "def build_joke_graph(writer_llm, critic_llm) -> CompiledStateGraph:\n",
517
+ " workflow = StateGraph(AgenticResearchArticleState)\n",
518
+ "\n",
519
+ " # Register nodes\n",
520
+ " workflow.add_node(\"show_menu\",show_menu)\n",
521
+ " workflow.add_node(\"update_category\",update_category)\n",
522
+ " workflow.add_node(\"exit_bot\",exit_bot)\n",
523
+ " workflow.add_node(\"writer\", make_writer_node(writer_llm))\n",
524
+ " workflow.add_node(\"critic\", make_critic_node(critic_llm))\n",
525
+ " workflow.add_node(\"show_final_article\",show_final_article)\n",
526
+ "\n",
527
+ " # Set entry point\n",
528
+ " workflow.set_entry_point(\"show_menu\")\n",
529
+ " \n",
530
+ " # Routing Logic-1\n",
531
+ " workflow.add_conditional_edges(\n",
532
+ " \"show_menu\",\n",
533
+ " route_choice,\n",
534
+ " {\n",
535
+ " \"fetch_article\": \"writer\",\n",
536
+ " \"update_category\": \"update_category\",\n",
537
+ " \"exit_bot\": \"exit_bot\", \n",
538
+ " },\n",
539
+ " )\n",
540
+ "\n",
541
+ " # Define transitions\n",
542
+ " workflow.add_edge(\"update_category\", \"show_menu\")\n",
543
+ " workflow.add_edge(\"writer\", \"critic\")\n",
544
+ " \n",
545
+ " \n",
546
+ " # Routing Logic-2\n",
547
+ " workflow.add_conditional_edges(\n",
548
+ " 'critic',\n",
549
+ " writer_critic_router,\n",
550
+ " {\n",
551
+ " \"writer\": 'writer',\n",
552
+ " \"show_final_article\": \"show_final_article\",\n",
553
+ " }\n",
554
+ " )\n",
555
+ " workflow.add_edge(\"show_final_article\", \"show_menu\")\n",
556
+ " workflow.add_edge(\"exit_bot\", END)\n",
557
+ " \n",
558
+ " return workflow.compile()"
559
+ ]
560
+ },
561
+ {
562
+ "cell_type": "markdown",
563
+ "id": "51a84f6c",
564
+ "metadata": {},
565
+ "source": [
566
+ "## **Graph Visualisation**"
567
+ ]
568
+ },
569
+ {
570
+ "cell_type": "code",
571
+ "execution_count": 19,
572
+ "id": "6446bea3",
573
+ "metadata": {},
574
+ "outputs": [],
575
+ "source": [
576
+ "def graph_visualiser(graph):\n",
577
+ " try:\n",
578
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
579
+ " except Exception as e:\n",
580
+ " print(e)"
581
+ ]
582
+ },
583
+ {
584
+ "cell_type": "code",
585
+ "execution_count": 20,
586
+ "id": "d4f1917b",
587
+ "metadata": {},
588
+ "outputs": [
589
+ {
590
+ "data": {
591
+ "image/png": "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",
592
+ "text/plain": [
593
+ "<IPython.core.display.Image object>"
594
+ ]
595
+ },
596
+ "metadata": {},
597
+ "output_type": "display_data"
598
+ }
599
+ ],
600
+ "source": [
601
+ "graph_visualiser(build_joke_graph(chat_model_writer,chat_model_critic))"
602
+ ]
603
+ },
604
+ {
605
+ "cell_type": "markdown",
606
+ "id": "754b0e32",
607
+ "metadata": {},
608
+ "source": [
609
+ "## **Main Code**"
610
+ ]
611
+ },
612
+ {
613
+ "cell_type": "code",
614
+ "execution_count": 21,
615
+ "id": "8d69b7e3",
616
+ "metadata": {},
617
+ "outputs": [],
618
+ "source": [
619
+ "def main():\n",
620
+ " print(\"\\n🎭 Starting article bot with writer–critic LLM loop...\")\n",
621
+ " graph = build_joke_graph(chat_model_writer,chat_model_critic)\n",
622
+ " final_state = graph.invoke(\n",
623
+ " AgenticResearchArticleState(category=\"nlp\",\n",
624
+ " title=\"Solving the myth and facts of child development with nlp\"), \n",
625
+ " config={\"recursion_limit\": 1000}\n",
626
+ " )\n",
627
+ " print(\"\\n✅ Done. Final Article Count:\", len(final_state[\"article\"]))"
628
+ ]
629
+ },
630
+ {
631
+ "cell_type": "code",
632
+ "execution_count": 22,
633
+ "id": "eca50538",
634
+ "metadata": {},
635
+ "outputs": [
636
+ {
637
+ "name": "stdout",
638
+ "output_type": "stream",
639
+ "text": [
640
+ "\n",
641
+ "🎭 Starting article bot with writer–critic LLM loop...\n",
642
+ "🎭 Menu | Category: NLP | Articles: 0\n",
643
+ "--------------------------------------------------\n",
644
+ "Pick an option\n",
645
+ "\n",
646
+ "CATEGORY: NLP\n",
647
+ "\n",
648
+ "\n",
649
+ "\"Solving the myth and facts of child development with nlp\"\n",
650
+ "\n",
651
+ "\n",
652
+ "this study delves into the intersection of natural language processing (nlp) and child development, a promising yet under-explored field. by leveraging advanced nlp techniques, we aim to uncover hidden patterns and insights in vast amounts of data related to child development, debunking common myths and confirming established facts. our research focuses on sentiment analysis of parental interactions, identifying developmental milestones from speech patterns, and understanding the impact of environmental factors on language development. the findings of this study could potentially revolutionize our understanding of child development and pave the way for personalized educational interventions.\n",
653
+ "\n",
654
+ "============================================================\n",
655
+ "🎭 Menu | Category: NLP | Articles: 1\n",
656
+ "--------------------------------------------------\n",
657
+ "Pick an option\n",
658
+ "\n",
659
+ "🚪==========================================================🚪\n",
660
+ " GOODBYE!\n",
661
+ "============================================================\n",
662
+ "\n",
663
+ "✅ Done. Final Article Count: 1\n"
664
+ ]
665
+ }
666
+ ],
667
+ "source": [
668
+ "main()"
669
+ ]
670
+ }
671
+ ],
672
+ "metadata": {
673
+ "kernelspec": {
674
+ "display_name": "Python 3",
675
+ "language": "python",
676
+ "name": "python3"
677
+ },
678
+ "language_info": {
679
+ "codemirror_mode": {
680
+ "name": "ipython",
681
+ "version": 3
682
+ },
683
+ "file_extension": ".py",
684
+ "mimetype": "text/x-python",
685
+ "name": "python",
686
+ "nbconvert_exporter": "python",
687
+ "pygments_lexer": "ipython3",
688
+ "version": "3.11.9"
689
+ }
690
+ },
691
+ "nbformat": 4,
692
+ "nbformat_minor": 5
693
+ }
notebooks/research_graph3.ipynb ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 92,
6
+ "id": "f342f56f",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import os\n",
11
+ "from typing import Optional\n",
12
+ "from pydantic import BaseModel, HttpUrl, ValidationError\n",
13
+ "from IPython.display import Image, display\n",
14
+ "from langgraph.graph import END, StateGraph\n",
15
+ "from langchain_community.document_loaders import SeleniumURLLoader\n",
16
+ "from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint\n",
17
+ "from langchain.prompts import PromptTemplate\n",
18
+ "from dotenv import load_dotenv"
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "code",
23
+ "execution_count": 93,
24
+ "id": "41a233ae",
25
+ "metadata": {},
26
+ "outputs": [
27
+ {
28
+ "data": {
29
+ "text/plain": [
30
+ "True"
31
+ ]
32
+ },
33
+ "execution_count": 93,
34
+ "metadata": {},
35
+ "output_type": "execute_result"
36
+ }
37
+ ],
38
+ "source": [
39
+ "load_dotenv('../.env') # Load environment variables"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": 94,
45
+ "id": "11d2a6f2",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "# ----------------------------\n",
50
+ "# State definition\n",
51
+ "# ----------------------------\n",
52
+ "class WebSearchState(BaseModel):\n",
53
+ " url: Optional[HttpUrl] = None\n",
54
+ " content: Optional[str] = None\n",
55
+ " summary: Optional[str] = None\n",
56
+ "\n",
57
+ "# Default URL to use initially\n",
58
+ "DEFAULT_URL = \"https://www.mdpi.com/2076-3417/11/20/9772\""
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": 95,
64
+ "id": "bafa9ce6",
65
+ "metadata": {},
66
+ "outputs": [],
67
+ "source": [
68
+ "def search_node(state: WebSearchState) -> dict:\n",
69
+ " # If URL already in state, summarize it first\n",
70
+ " if state.url:\n",
71
+ " print(f\"[Search] Using existing URL: {state.url}\")\n",
72
+ " return {\"url\": str(state.url)}\n",
73
+ "\n",
74
+ " # Prompt user for URL with default shown\n",
75
+ " user_input = input(f\"🔗 Please enter a URL to summarize [default: {DEFAULT_URL}]: \").strip()\n",
76
+ " url_input = user_input if user_input else DEFAULT_URL\n",
77
+ "\n",
78
+ " # Validate URL\n",
79
+ " try:\n",
80
+ " validated_state = WebSearchState(url=url_input)\n",
81
+ " except ValidationError as e:\n",
82
+ " raise ValueError(f\"❌ Invalid URL: {e}\")\n",
83
+ "\n",
84
+ " print(f\"[Search] Using URL: {validated_state.url}\")\n",
85
+ " return {\"url\": str(validated_state.url)}"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "code",
90
+ "execution_count": 96,
91
+ "id": "5de07a81",
92
+ "metadata": {},
93
+ "outputs": [],
94
+ "source": [
95
+ "# ----------------------------\n",
96
+ "# 2. Load Webpage Node\n",
97
+ "# ----------------------------\n",
98
+ "def load_node(state: WebSearchState) -> dict:\n",
99
+ " # Limit content length to ~100,000 characters (≈ 32,000 tokens max)\n",
100
+ " MAX_CHARS = 30000\n",
101
+ " url = state.url\n",
102
+ " if not url:\n",
103
+ " return {\"content\": \"No URL to load\"}\n",
104
+ " loader = SeleniumURLLoader(urls=[str(url)])\n",
105
+ " docs = loader.load()\n",
106
+ " content = docs[0].page_content if docs else \"No content loaded\"\n",
107
+ " # Truncate early to prevent overload later\n",
108
+ " truncated_content = content[:MAX_CHARS]\n",
109
+ " print(f\"[Load] Loaded {len(truncated_content)} characters\")\n",
110
+ " return {\"content\": content}"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "code",
115
+ "execution_count": 97,
116
+ "id": "ffa2a1a0",
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "# ----------------------------\n",
121
+ "# 3. Summarize Node\n",
122
+ "# ----------------------------\n",
123
+ "# loadding Huggingface token\n",
124
+ "HUGGINGFACEHUB_API_TOKEN = os.getenv(\"HUGGINGFACEHUB_API_TOKEN\")\n",
125
+ "repo_id = \"mistralai/Mistral-7B-Instruct-v0.3\"\n",
126
+ "model_kwargs = {\"temperature\": 0.1, \n",
127
+ " \"max_new_tokens\": 100, # Maximum tokens to generate\n",
128
+ " \"timeout\": 6000}\n",
129
+ "\n",
130
+ "llm = HuggingFaceEndpoint(\n",
131
+ " repo_id=repo_id,\n",
132
+ " huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN,\n",
133
+ " **model_kwargs\n",
134
+ ")\n",
135
+ "chat_model = ChatHuggingFace(llm=llm)\n",
136
+ "\n",
137
+ "WRITER_PROMPT = \"\"\"\n",
138
+ "You are an expert summarizer with 20 years of experience.\n",
139
+ "Read the following webpage content \n",
140
+ "carefully and produce a detailed \n",
141
+ "but concise summary that captures key points clearly.\n",
142
+ "\n",
143
+ "Webpage content:\n",
144
+ "{content}\n",
145
+ "\n",
146
+ "Summary:\n",
147
+ "\"\"\"\n",
148
+ "prompt = PromptTemplate.from_template(WRITER_PROMPT)\n",
149
+ "summarize_chain = prompt | chat_model\n",
150
+ "\n",
151
+ "def summarize_node(state: WebSearchState) -> dict:\n",
152
+ " if not state.content:\n",
153
+ " return {\"summary\": \"No content to summarize\"}\n",
154
+ " result = summarize_chain.invoke({\"content\": state.content})\n",
155
+ " result = result.content\n",
156
+ " print(\"[Summarize] Done\")\n",
157
+ " # print(result)\n",
158
+ " return {\"summary\": result}"
159
+ ]
160
+ },
161
+ {
162
+ "cell_type": "code",
163
+ "execution_count": 98,
164
+ "id": "ea51e28d",
165
+ "metadata": {},
166
+ "outputs": [],
167
+ "source": [
168
+ "# ----------------------------\n",
169
+ "# LangGraph Construction\n",
170
+ "# ----------------------------\n",
171
+ "builder = StateGraph(WebSearchState)\n",
172
+ "builder.add_node(\"search\", search_node)\n",
173
+ "builder.add_node(\"load\", load_node)\n",
174
+ "builder.add_node(\"summarize\", summarize_node)\n",
175
+ "\n",
176
+ "builder.set_entry_point(\"search\")\n",
177
+ "builder.add_edge(\"search\", \"load\")\n",
178
+ "builder.add_edge(\"load\", \"summarize\")\n",
179
+ "builder.add_edge(\"summarize\", END)\n",
180
+ "\n",
181
+ "graph = builder.compile()"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "code",
186
+ "execution_count": 99,
187
+ "id": "39b02b00",
188
+ "metadata": {},
189
+ "outputs": [
190
+ {
191
+ "data": {
192
+ "image/png": "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",
193
+ "text/plain": [
194
+ "<IPython.core.display.Image object>"
195
+ ]
196
+ },
197
+ "metadata": {},
198
+ "output_type": "display_data"
199
+ }
200
+ ],
201
+ "source": [
202
+ "def graph_visualiser(graph):\n",
203
+ " try:\n",
204
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
205
+ " except Exception as e:\n",
206
+ " print(e)\n",
207
+ "\n",
208
+ "graph_visualiser(graph=graph)"
209
+ ]
210
+ },
211
+ {
212
+ "cell_type": "code",
213
+ "execution_count": 100,
214
+ "id": "47efdcd7",
215
+ "metadata": {},
216
+ "outputs": [
217
+ {
218
+ "name": "stdout",
219
+ "output_type": "stream",
220
+ "text": [
221
+ "\n",
222
+ "--- Generating summary for default URL ---\n",
223
+ "[Search] Using existing URL: https://www.mdpi.com/2076-3417/11/20/9772\n",
224
+ "[Load] Loaded 30000 characters\n"
225
+ ]
226
+ },
227
+ {
228
+ "name": "stderr",
229
+ "output_type": "stream",
230
+ "text": [
231
+ "C:\\Users\\DANNY\\AppData\\Local\\Temp\\ipykernel_9704\\1414824205.py:8: PydanticDeprecatedSince20: The `parse_obj` method is deprecated; use `model_validate` instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.11/migration/\n",
232
+ " final_state_model = WebSearchState.parse_obj(final_state)\n"
233
+ ]
234
+ },
235
+ {
236
+ "name": "stdout",
237
+ "output_type": "stream",
238
+ "text": [
239
+ "[Summarize] Done\n",
240
+ "\n",
241
+ "URL: https://www.mdpi.com/2076-3417/11/20/9772\n",
242
+ "SUMMARY:\n",
243
+ " The article discusses the design of a gas cyclone using a hybrid particle swarm optimization (PSO) algorithm. The research aims to simplify complex mathematical models and the sensitivity approach for gas cyclone design with the use of an objective function, which is of the minimization type. The process makes use of the initial population generated by the DE algorithm, and the stopping criterion of DE is set as the fitness value. When the fitness value is not less than the current global best, the DE population is taken over by PSO. For each iteration, the new velocity and position are updated in every generation until the optimal solution is achieved. The hybrid DEPSO method first reduces the search space using the DE algorithm, and then the obtained populations are used as the initial population by the PSO to achieve a fast convergence rate to a final global optimum. The proposed DEPSO method is compared to PSO and DE algorithms, and it is found that DEPSO solves the issue of premature convergence in PSO by utilizing the crossover operator of DE to improve the distribution of information between candidate solutions. The optimized cyclone geometry, using PSO, DE, and hybrid DEPSO algorithms, is shown, and it is concluded that DEPSO had a better cost value but lower efficiency compared to PSO and DE algorithms. The research received no external funding and the authors declare no conflict of interest.\n",
244
+ "\n",
245
+ "Keywords:\n",
246
+ " - particle swarm optimization (PSO)\n",
247
+ " - differential evolution (DE)\n",
248
+ " - gas cyclone\n",
249
+ " - hybridised particle swarm optimization\n",
250
+ " - evolutionary algorithm\n",
251
+ "\n",
252
+ "References:\n",
253
+ " - Kennedy, J.; Eberhart, R. Particle Swarm Optimization. In Proceedings of the IEEE international Conference on Neural Networks, Perth, WA, Australia, 27 November–1 December 1995; Volume 4, pp. 1942–1948.\n",
254
+ " - Storn, R.; Price, K. Differential Evolution—A Simple and Efficient Heuristic for global Optimization over Continuous Spaces. J. Glob. Optim. 1997, 11, 341–359.\n",
255
+ " - Liu, J.; Lampinen, J. On setting the control parameter of the differential evolution method. In Proceedings of the 8th International Conference on Soft Computing (MENDEL’02), Brno, Czech Republic, 8–10 June 2002; pp. 11–18.\n",
256
+ " - Tang, Y.; Gao, H.; Zou, W.; Kurths, J. Identifying controlling nodes in neuronal networks in different scales. PLoS ONE 2012, 7, e41375.\n",
257
+ " - Storn, R. Differential evolution design of an IIR-filter. In Proceedings of the 1996 IEEE International Conference on Evolutionary Computation (ICEC’96), Nagoya, Japan, 20–22 May 1996; pp. 268–273.\n",
258
+ " - Varadarajan, M.; Swarup, K. Differential evolution approach for optimal reactive power dispatch. Appl. Soft Comput. 2008, 8, 1549–1561.\n",
259
+ " - Mallipeddi, R.; Suganthan, P.N. Differential evolution algorithm with ensemble of parameters and mutation and crossover strategies. In Proceedings of the Swarm Evolutionary and Memetic Computing Conference, Chennai, India, 16–18 December 2010; pp. 71–78.\n",
260
+ " - Brest, J.; Greiner, S.; Bôskovi’c, B.; Mernik, M.; Zumer, V. Self-adapting control parameters in differential evolution: A comparative study on numerical benchmark problems. IEEE Trans. Evol. Comput. 2006, 10, 646–657.\n",
261
+ " - Yang, Z.; Tang, K.; Yao, X. Self-adaptive differential evolution with neighborhood search. In Proceedings of the IEEE Congress on Evolutionary Computation (CEC’08), Hong Kong, China, 1–6 June 2008; pp. 1110–1116.\n",
262
+ " - Zhang, J.; Sanderson, A.C. JADE: Adaptive Differential Evolution with Optional External Archive. In IEEE Transactions on Evolutionary Computation; IEEE: Manhattan, NY, USA, 2009; Volume 13, pp. 945–958.\n",
263
+ " - Pant, M.; Thangaraj, R.; Abraham, A. DE-PSO: A new hybrid meta-heuristic for solving global optimization problems. Electr. Power Syst. Res. 2004, 70, 203–210.\n",
264
+ " - Talbi, H.; Batouche, M. Hybrid particle swarm with differential evolution for multimodal image registration. In Proceedings of the IEEE International Conference on Industrial Technology (ICIT ‘04), Hammamet, Tunisia, 1 December 2004; pp. 1567–1572.\n",
265
+ " - Omran, M.G.H.; Engelbrecht, A.P.; Salman, A. Differential evolution based particle swarm optimization. In Proceedings of the IEEE Swarm Intelligence Symposium (SIS ‘07), Honolulu, HI, USA, 1–5 April 2007; pp. 112–119.\n",
266
+ " - Hao, Z.F.; Guo, G.H.; Huang, H. A particle swarm optimization algorithm with differential evolution. In Proceedings of the 6th International Conference on Machine Learning and Cybernetics (ICMLC ‘07), Hong Kong, China, 19–22 August 2007; pp. 1031–1035.\n",
267
+ " - Wang, L. Theoretical Study of Cyclone Design. Ph.D. Thesis, Texas A&M University, College Station, TX, USA, 2004. Available online: https://core.ac.uk/download/pdf/147123938.pdf (accessed on 2 October 2021).\n",
268
+ " - Mao, B.; Xie, Z.; Wang, Y.; Handroos, H.; Wu, H. A Hybrid Strategy of Differential Evolution and Modified Particle Swarm Optimization for Numerical Solution of a Parallel Manipulator. Math. Probl. Eng. 2018, 2018, 1–9.\n",
269
+ " - Wang, L.; Parnell, C.B.; Shaw, B.W. 1D2D, 1D3D, 2D2D cyclone fractional efficiency curves for fine dust. In Proceedings of the 2000 Beltwide Cotton Conferences, San Antonio, TX, USA, 4−8 January 2000.\n",
270
+ " - Wang, L.; Parnell, C.B.; Shaw, B.W. A New Theoretical Approach for Predicting Number of Turns and Cyclone Pressure Drop; ASAE: Washington, DC, USA; St. Joseph, MI, USA, 2001.\n",
271
+ " - Wang, L.; Parnell, C.B.; Oemler, J.A.; Shaw, B.W.; Lacey, R.E. Analysis of Cyclone Collection Efficiency; ASAE: Washington, DC, USA; St. Joseph, MI, USA, 2003.\n",
272
+ " - Lapple, C.E. Processes use many collector types. Chem. Eng. 1951, 58, 144–151.\n",
273
+ " - Müller, P.K.S.; Airaghi, S.; Marchetto, J. Optimization algorithms based on a model of bacterial chemotaxis. In From Animals to Animats 6: Proceedings of the Sixth International Conference on Simulation of Adaptive Behavior, 1st ed.; A Bradford Book: London, UK, 2000; pp. 375–384. Available online: https://direct.mit.edu/books/book/4493/From-Animals-to-Animats-6Proceedings-of-the-Sixth (accessed on 2 October 2021).\n",
274
+ " - Passino, K.M. Biomimicry of bacterial foraging for distributed optimization and control. IEEE Control. Syst. Mag. 2002, 22, 52–67.\n",
275
+ " - Karaboga, D.; Basturk, B. Artificial Bee Colony (ABC) optimization algorithm\n",
276
+ "Exiting. Goodbye!\n",
277
+ "[Search] Using existing URL: https://www.geeksforgeeks.org/machine-learning/getting-started-with-transformers/\n",
278
+ "[Load] Loaded 30000 characters\n"
279
+ ]
280
+ },
281
+ {
282
+ "ename": "HfHubHTTPError",
283
+ "evalue": "402 Client Error: Payment Required for url: https://router.huggingface.co/together/v1/chat/completions (Request ID: Root=1-686fdd94-63190e1c129ec1a04138b33e;4cdc7b66-a1fe-4e32-901f-ee8dc7eb98c7)\n\nYou have exceeded your monthly included credits for Inference Providers. Subscribe to PRO to get 20x more monthly included credits.",
284
+ "output_type": "error",
285
+ "traceback": [
286
+ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
287
+ "\u001b[1;31mHTTPError\u001b[0m Traceback (most recent call last)",
288
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\huggingface_hub\\utils\\_http.py:409\u001b[0m, in \u001b[0;36mhf_raise_for_status\u001b[1;34m(response, endpoint_name)\u001b[0m\n\u001b[0;32m 408\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 409\u001b[0m \u001b[43mresponse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mraise_for_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 410\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m HTTPError \u001b[38;5;28;01mas\u001b[39;00m e:\n",
289
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\requests\\models.py:1024\u001b[0m, in \u001b[0;36mResponse.raise_for_status\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 1023\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m http_error_msg:\n\u001b[1;32m-> 1024\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m HTTPError(http_error_msg, response\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m)\n",
290
+ "\u001b[1;31mHTTPError\u001b[0m: 402 Client Error: Payment Required for url: https://router.huggingface.co/together/v1/chat/completions",
291
+ "\nThe above exception was the direct cause of the following exception:\n",
292
+ "\u001b[1;31mHfHubHTTPError\u001b[0m Traceback (most recent call last)",
293
+ "Cell \u001b[1;32mIn[100], line 25\u001b[0m\n\u001b[0;32m 22\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid URL entered: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 23\u001b[0m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[1;32m---> 25\u001b[0m final_state \u001b[38;5;241m=\u001b[39m \u001b[43mgraph\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43muser_state\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 26\u001b[0m final_state_model \u001b[38;5;241m=\u001b[39m WebSearchState\u001b[38;5;241m.\u001b[39mparse_obj(final_state)\n\u001b[0;32m 27\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mURL:\u001b[39m\u001b[38;5;124m\"\u001b[39m, final_state_model\u001b[38;5;241m.\u001b[39murl)\n",
294
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\pregel\\__init__.py:2823\u001b[0m, in \u001b[0;36mPregel.invoke\u001b[1;34m(self, input, config, stream_mode, output_keys, interrupt_before, interrupt_after, checkpoint_during, debug, **kwargs)\u001b[0m\n\u001b[0;32m 2820\u001b[0m chunks: \u001b[38;5;28mlist\u001b[39m[Union[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any], Any]] \u001b[38;5;241m=\u001b[39m []\n\u001b[0;32m 2821\u001b[0m interrupts: \u001b[38;5;28mlist\u001b[39m[Interrupt] \u001b[38;5;241m=\u001b[39m []\n\u001b[1;32m-> 2823\u001b[0m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2824\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2825\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2826\u001b[0m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2827\u001b[0m \u001b[43m \u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2828\u001b[0m \u001b[43m \u001b[49m\u001b[43minterrupt_before\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minterrupt_before\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2829\u001b[0m \u001b[43m \u001b[49m\u001b[43minterrupt_after\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minterrupt_after\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2830\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheckpoint_during\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheckpoint_during\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2831\u001b[0m \u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdebug\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2832\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2833\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[0;32m 2834\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m==\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mvalues\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\n\u001b[0;32m 2835\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2836\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43misinstance\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mdict\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 2837\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;129;43;01mand\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mints\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m:=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43mINTERRUPT\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mis\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\n\u001b[0;32m 2838\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n",
295
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\pregel\\__init__.py:2461\u001b[0m, in \u001b[0;36mPregel.stream\u001b[1;34m(self, input, config, stream_mode, output_keys, interrupt_before, interrupt_after, checkpoint_during, debug, subgraphs)\u001b[0m\n\u001b[0;32m 2455\u001b[0m \u001b[38;5;66;03m# Similarly to Bulk Synchronous Parallel / Pregel model\u001b[39;00m\n\u001b[0;32m 2456\u001b[0m \u001b[38;5;66;03m# computation proceeds in steps, while there are channel updates.\u001b[39;00m\n\u001b[0;32m 2457\u001b[0m \u001b[38;5;66;03m# Channel updates from step N are only visible in step N+1\u001b[39;00m\n\u001b[0;32m 2458\u001b[0m \u001b[38;5;66;03m# channels are guaranteed to be immutable for the duration of the step,\u001b[39;00m\n\u001b[0;32m 2459\u001b[0m \u001b[38;5;66;03m# with channel updates applied only at the transition between steps.\u001b[39;00m\n\u001b[0;32m 2460\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m loop\u001b[38;5;241m.\u001b[39mtick(input_keys\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39minput_channels):\n\u001b[1;32m-> 2461\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrunner\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtick\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 2462\u001b[0m \u001b[43m \u001b[49m\u001b[43mloop\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtasks\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2463\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstep_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2464\u001b[0m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2465\u001b[0m \u001b[43m \u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mget_waiter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 2466\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[0;32m 2467\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# emit output\u001b[39;49;00m\n\u001b[0;32m 2468\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield from\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 2469\u001b[0m \u001b[38;5;66;03m# emit output\u001b[39;00m\n",
296
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\pregel\\runner.py:153\u001b[0m, in \u001b[0;36mPregelRunner.tick\u001b[1;34m(self, tasks, reraise, timeout, retry_policy, get_waiter)\u001b[0m\n\u001b[0;32m 151\u001b[0m t \u001b[38;5;241m=\u001b[39m tasks[\u001b[38;5;241m0\u001b[39m]\n\u001b[0;32m 152\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 153\u001b[0m \u001b[43mrun_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 154\u001b[0m \u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 155\u001b[0m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 156\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfigurable\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\n\u001b[0;32m 157\u001b[0m \u001b[43m \u001b[49m\u001b[43mCONFIG_KEY_CALL\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 158\u001b[0m \u001b[43m \u001b[49m\u001b[43m_call\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 159\u001b[0m \u001b[43m \u001b[49m\u001b[43mweakref\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mt\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 160\u001b[0m \u001b[43m \u001b[49m\u001b[43mretry\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 161\u001b[0m \u001b[43m \u001b[49m\u001b[43mfutures\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mweakref\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 162\u001b[0m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mschedule_task\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 163\u001b[0m \u001b[43m \u001b[49m\u001b[43msubmit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msubmit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 164\u001b[0m \u001b[43m \u001b[49m\u001b[43mreraise\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mreraise\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 165\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 166\u001b[0m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 167\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 168\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcommit(t, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[0;32m 169\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n",
297
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\pregel\\retry.py:40\u001b[0m, in \u001b[0;36mrun_with_retry\u001b[1;34m(task, retry_policy, configurable)\u001b[0m\n\u001b[0;32m 38\u001b[0m task\u001b[38;5;241m.\u001b[39mwrites\u001b[38;5;241m.\u001b[39mclear()\n\u001b[0;32m 39\u001b[0m \u001b[38;5;66;03m# run the task\u001b[39;00m\n\u001b[1;32m---> 40\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtask\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mproc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtask\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minput\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 41\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m ParentCommand \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[0;32m 42\u001b[0m ns: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m=\u001b[39m config[CONF][CONFIG_KEY_CHECKPOINT_NS]\n",
298
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\utils\\runnable.py:623\u001b[0m, in \u001b[0;36mRunnableSeq.invoke\u001b[1;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[0;32m 621\u001b[0m \u001b[38;5;66;03m# run in context\u001b[39;00m\n\u001b[0;32m 622\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config, run) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[1;32m--> 623\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mcontext\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 624\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 625\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m step\u001b[38;5;241m.\u001b[39minvoke(\u001b[38;5;28minput\u001b[39m, config)\n",
299
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langgraph\\utils\\runnable.py:377\u001b[0m, in \u001b[0;36mRunnableCallable.invoke\u001b[1;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[0;32m 375\u001b[0m run_manager\u001b[38;5;241m.\u001b[39mon_chain_end(ret)\n\u001b[0;32m 376\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m--> 377\u001b[0m ret \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 378\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrecurse \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(ret, Runnable):\n\u001b[0;32m 379\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m ret\u001b[38;5;241m.\u001b[39minvoke(\u001b[38;5;28minput\u001b[39m, config)\n",
300
+ "Cell \u001b[1;32mIn[97], line 35\u001b[0m, in \u001b[0;36msummarize_node\u001b[1;34m(state)\u001b[0m\n\u001b[0;32m 33\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m state\u001b[38;5;241m.\u001b[39mcontent:\n\u001b[0;32m 34\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msummary\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNo content to summarize\u001b[39m\u001b[38;5;124m\"\u001b[39m}\n\u001b[1;32m---> 35\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43msummarize_chain\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcontent\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcontent\u001b[49m\u001b[43m}\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 36\u001b[0m result \u001b[38;5;241m=\u001b[39m result\u001b[38;5;241m.\u001b[39mcontent\n\u001b[0;32m 37\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m[Summarize] Done\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
301
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_core\\runnables\\base.py:3034\u001b[0m, in \u001b[0;36mRunnableSequence.invoke\u001b[1;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[0;32m 3032\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m context\u001b[38;5;241m.\u001b[39mrun(step\u001b[38;5;241m.\u001b[39minvoke, \u001b[38;5;28minput\u001b[39m, config, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m 3033\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 3034\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m context\u001b[38;5;241m.\u001b[39mrun(step\u001b[38;5;241m.\u001b[39minvoke, \u001b[38;5;28minput\u001b[39m, config)\n\u001b[0;32m 3035\u001b[0m \u001b[38;5;66;03m# finish the root run\u001b[39;00m\n\u001b[0;32m 3036\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n",
302
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_core\\language_models\\chat_models.py:370\u001b[0m, in \u001b[0;36mBaseChatModel.invoke\u001b[1;34m(self, input, config, stop, **kwargs)\u001b[0m\n\u001b[0;32m 358\u001b[0m \u001b[38;5;129m@override\u001b[39m\n\u001b[0;32m 359\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21minvoke\u001b[39m(\n\u001b[0;32m 360\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 365\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[0;32m 366\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m BaseMessage:\n\u001b[0;32m 367\u001b[0m config \u001b[38;5;241m=\u001b[39m ensure_config(config)\n\u001b[0;32m 368\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m cast(\n\u001b[0;32m 369\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mChatGeneration\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m--> 370\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgenerate_prompt\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 371\u001b[0m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_convert_input\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 372\u001b[0m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 373\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcallbacks\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 374\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtags\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 375\u001b[0m \u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mmetadata\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 376\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrun_name\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 377\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrun_id\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 378\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 379\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mgenerations[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m],\n\u001b[0;32m 380\u001b[0m )\u001b[38;5;241m.\u001b[39mmessage\n",
303
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_core\\language_models\\chat_models.py:947\u001b[0m, in \u001b[0;36mBaseChatModel.generate_prompt\u001b[1;34m(self, prompts, stop, callbacks, **kwargs)\u001b[0m\n\u001b[0;32m 938\u001b[0m \u001b[38;5;129m@override\u001b[39m\n\u001b[0;32m 939\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mgenerate_prompt\u001b[39m(\n\u001b[0;32m 940\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 944\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[0;32m 945\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m LLMResult:\n\u001b[0;32m 946\u001b[0m prompt_messages \u001b[38;5;241m=\u001b[39m [p\u001b[38;5;241m.\u001b[39mto_messages() \u001b[38;5;28;01mfor\u001b[39;00m p \u001b[38;5;129;01min\u001b[39;00m prompts]\n\u001b[1;32m--> 947\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgenerate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprompt_messages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
304
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_core\\language_models\\chat_models.py:766\u001b[0m, in \u001b[0;36mBaseChatModel.generate\u001b[1;34m(self, messages, stop, callbacks, tags, metadata, run_name, run_id, **kwargs)\u001b[0m\n\u001b[0;32m 763\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(input_messages):\n\u001b[0;32m 764\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 765\u001b[0m results\u001b[38;5;241m.\u001b[39mappend(\n\u001b[1;32m--> 766\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_generate_with_cache\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 767\u001b[0m \u001b[43m \u001b[49m\u001b[43mm\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 768\u001b[0m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 769\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrun_managers\u001b[49m\u001b[43m[\u001b[49m\u001b[43mi\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrun_managers\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 770\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 771\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 772\u001b[0m )\n\u001b[0;32m 773\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 774\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_managers:\n",
305
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_core\\language_models\\chat_models.py:1012\u001b[0m, in \u001b[0;36mBaseChatModel._generate_with_cache\u001b[1;34m(self, messages, stop, run_manager, **kwargs)\u001b[0m\n\u001b[0;32m 1010\u001b[0m result \u001b[38;5;241m=\u001b[39m generate_from_stream(\u001b[38;5;28miter\u001b[39m(chunks))\n\u001b[0;32m 1011\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m inspect\u001b[38;5;241m.\u001b[39msignature(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_generate)\u001b[38;5;241m.\u001b[39mparameters\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m-> 1012\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_generate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 1013\u001b[0m \u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[0;32m 1014\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1015\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 1016\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_generate(messages, stop\u001b[38;5;241m=\u001b[39mstop, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
306
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\langchain_huggingface\\chat_models\\huggingface.py:574\u001b[0m, in \u001b[0;36mChatHuggingFace._generate\u001b[1;34m(self, messages, stop, run_manager, stream, **kwargs)\u001b[0m\n\u001b[0;32m 567\u001b[0m message_dicts, params \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_create_message_dicts(messages, stop)\n\u001b[0;32m 568\u001b[0m params \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 569\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstop\u001b[39m\u001b[38;5;124m\"\u001b[39m: stop,\n\u001b[0;32m 570\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mparams,\n\u001b[0;32m 571\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstream\u001b[39m\u001b[38;5;124m\"\u001b[39m: stream} \u001b[38;5;28;01mif\u001b[39;00m stream \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01melse\u001b[39;00m {}),\n\u001b[0;32m 572\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 573\u001b[0m }\n\u001b[1;32m--> 574\u001b[0m answer \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mllm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchat_completion\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmessages\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmessage_dicts\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mparams\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 575\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_create_chat_result(answer)\n\u001b[0;32m 576\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n",
307
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\huggingface_hub\\inference\\_client.py:923\u001b[0m, in \u001b[0;36mInferenceClient.chat_completion\u001b[1;34m(self, messages, model, stream, frequency_penalty, logit_bias, logprobs, max_tokens, n, presence_penalty, response_format, seed, stop, stream_options, temperature, tool_choice, tool_prompt, tools, top_logprobs, top_p, extra_body)\u001b[0m\n\u001b[0;32m 895\u001b[0m parameters \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 896\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmodel\u001b[39m\u001b[38;5;124m\"\u001b[39m: payload_model,\n\u001b[0;32m 897\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfrequency_penalty\u001b[39m\u001b[38;5;124m\"\u001b[39m: frequency_penalty,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 914\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m(extra_body \u001b[38;5;129;01mor\u001b[39;00m {}),\n\u001b[0;32m 915\u001b[0m }\n\u001b[0;32m 916\u001b[0m request_parameters \u001b[38;5;241m=\u001b[39m provider_helper\u001b[38;5;241m.\u001b[39mprepare_request(\n\u001b[0;32m 917\u001b[0m inputs\u001b[38;5;241m=\u001b[39mmessages,\n\u001b[0;32m 918\u001b[0m parameters\u001b[38;5;241m=\u001b[39mparameters,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 921\u001b[0m api_key\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtoken,\n\u001b[0;32m 922\u001b[0m )\n\u001b[1;32m--> 923\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_inner_post\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest_parameters\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 925\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m stream:\n\u001b[0;32m 926\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _stream_chat_completion_response(data) \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n",
308
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\huggingface_hub\\inference\\_client.py:279\u001b[0m, in \u001b[0;36mInferenceClient._inner_post\u001b[1;34m(self, request_parameters, stream)\u001b[0m\n\u001b[0;32m 276\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m InferenceTimeoutError(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInference call timed out: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrequest_parameters\u001b[38;5;241m.\u001b[39murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merror\u001b[39;00m \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[0;32m 278\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 279\u001b[0m \u001b[43mhf_raise_for_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 280\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m response\u001b[38;5;241m.\u001b[39miter_lines() \u001b[38;5;28;01mif\u001b[39;00m stream \u001b[38;5;28;01melse\u001b[39;00m response\u001b[38;5;241m.\u001b[39mcontent\n\u001b[0;32m 281\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m HTTPError \u001b[38;5;28;01mas\u001b[39;00m error:\n",
309
+ "File \u001b[1;32mc:\\Users\\DANNY\\Desktop\\llmai\\llm_deep\\Lib\\site-packages\\huggingface_hub\\utils\\_http.py:482\u001b[0m, in \u001b[0;36mhf_raise_for_status\u001b[1;34m(response, endpoint_name)\u001b[0m\n\u001b[0;32m 478\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m _format(HfHubHTTPError, message, response) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[0;32m 480\u001b[0m \u001b[38;5;66;03m# Convert `HTTPError` into a `HfHubHTTPError` to display request information\u001b[39;00m\n\u001b[0;32m 481\u001b[0m \u001b[38;5;66;03m# as well (request id and/or server error message)\u001b[39;00m\n\u001b[1;32m--> 482\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m _format(HfHubHTTPError, \u001b[38;5;28mstr\u001b[39m(e), response) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n",
310
+ "\u001b[1;31mHfHubHTTPError\u001b[0m: 402 Client Error: Payment Required for url: https://router.huggingface.co/together/v1/chat/completions (Request ID: Root=1-686fdd94-63190e1c129ec1a04138b33e;4cdc7b66-a1fe-4e32-901f-ee8dc7eb98c7)\n\nYou have exceeded your monthly included credits for Inference Providers. Subscribe to PRO to get 20x more monthly included credits.",
311
+ "\u001b[0mDuring task with name 'summarize' and id 'ca8d9b22-54c9-a9e6-e4d3-512d5d9c7d2f'"
312
+ ]
313
+ }
314
+ ],
315
+ "source": [
316
+ "# ----------------------------\n",
317
+ "# Run It\n",
318
+ "# ----------------------------\n",
319
+ "# Run once with default URL to get initial summary\n",
320
+ "print(\"\\n--- Generating summary for default URL ---\")\n",
321
+ "initial_state = WebSearchState(url=DEFAULT_URL)\n",
322
+ "final_state = graph.invoke(initial_state)\n",
323
+ "final_state_model = WebSearchState.parse_obj(final_state)\n",
324
+ "print(\"\\nURL:\", final_state_model.url)\n",
325
+ "print(\"SUMMARY:\\n\", final_state_model.summary)\n",
326
+ "\n",
327
+ "\n",
328
+ "# Now prompt user to enter a new URL or leave blank to keep previous summary\n",
329
+ "while True:\n",
330
+ " user_input = input(\"\\nEnter a new URL to summarize or press Enter to exit: \").strip()\n",
331
+ " if not user_input:\n",
332
+ " print(\"Exiting. Goodbye!\")\n",
333
+ " break\n",
334
+ " try:\n",
335
+ " user_state = WebSearchState(url=user_input)\n",
336
+ " except ValidationError as e:\n",
337
+ " print(f\"Invalid URL entered: {e}\")\n",
338
+ " continue\n",
339
+ "\n",
340
+ "final_state = graph.invoke(user_state)\n",
341
+ "final_state_model = WebSearchState.parse_obj(final_state)\n",
342
+ "print(\"\\nURL:\", final_state_model.url)\n",
343
+ "print(\"SUMMARY:\\n\", final_state_model.summary)"
344
+ ]
345
+ }
346
+ ],
347
+ "metadata": {
348
+ "kernelspec": {
349
+ "display_name": "Python 3",
350
+ "language": "python",
351
+ "name": "python3"
352
+ },
353
+ "language_info": {
354
+ "codemirror_mode": {
355
+ "name": "ipython",
356
+ "version": 3
357
+ },
358
+ "file_extension": ".py",
359
+ "mimetype": "text/x-python",
360
+ "name": "python",
361
+ "nbconvert_exporter": "python",
362
+ "pygments_lexer": "ipython3",
363
+ "version": "3.11.9"
364
+ }
365
+ },
366
+ "nbformat": 4,
367
+ "nbformat_minor": 5
368
+ }
pytest.ini ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [pytest]
2
+ pythonpath = .
3
+ testpaths = tests
4
+ filterwarnings =
5
+ ignore::DeprecationWarning
6
+ ignore::UserWarning
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.45
2
+ langgraph>=0.6.4
3
+ langchain==0.3.25
4
+ langchain-huggingface==0.2.0
5
+ transformers==4.51.3
6
+ huggingface-hub
7
+ torch
8
+ python-dotenv==1.1.0
9
+ pydantic
10
+ gradio==5.42.0
shared.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, HttpUrl
2
+ from typing import Optional
3
+
4
+ class ResearchState(BaseModel):
5
+ # For article graph
6
+ input: Optional[str] = None
7
+ category: Optional[str] = None
8
+ abstract: Optional[str] = None
9
+ critique: Optional[str] = None
10
+ final_abstract: Optional[str] = None
11
+
12
+ # For web summarizer graph
13
+ url: Optional[HttpUrl] = None
14
+ content: Optional[str] = None
15
+ summary: Optional[str] = None
tests/conftest.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import importlib
4
+ import pytest
5
+ from dotenv import load_dotenv
6
+
7
+ # Ensure project root is importable when tests run from anywhere
8
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
9
+ if PROJECT_ROOT not in sys.path:
10
+ sys.path.insert(0, PROJECT_ROOT)
11
+
12
+ load_dotenv() # Load env vars for tests
13
+
14
+
15
+ class _MockResponse:
16
+ def __init__(self, content: str):
17
+ self.content = content
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ def no_real_llm_calls(monkeypatch):
22
+ """
23
+ Autouse fixture that prevents real LLM/endpoint calls during tests
24
+ by monkeypatching chain-like objects to return dummy responses.
25
+
26
+ This tries to import modules and patches attributes on module objects
27
+ instead of using dotted-name strings (which cause import attempts
28
+ and can raise ModuleNotFoundError).
29
+ """
30
+
31
+ # Helper to safely import a module name, return None on failure
32
+ def try_import(name):
33
+ try:
34
+ return importlib.import_module(name)
35
+ except ModuleNotFoundError:
36
+ return None
37
+
38
+ # 1) Patch writer_chain in graph_article.writer if present
39
+ writer_mod = try_import("graph_article.writer")
40
+ if writer_mod is not None:
41
+ monkeypatch.setattr(
42
+ writer_mod,
43
+ "writer_chain",
44
+ lambda *a, **kw: _MockResponse("Mocked writer response"),
45
+ raising=False,
46
+ )
47
+
48
+ # 2) Patch critic_chain in graph_article.critic if present
49
+ critic_mod = try_import("graph_article.critic")
50
+ if critic_mod is not None:
51
+ monkeypatch.setattr(
52
+ critic_mod,
53
+ "critic_chain",
54
+ lambda *a, **kw: _MockResponse("ACCEPTED"),
55
+ raising=False,
56
+ )
57
+
58
+ # 3) Patch summarizer: try both spellings
59
+ summarizer_mod = try_import("graph_web.summarizer") or try_import("graph_web.summariser")
60
+ if summarizer_mod is not None:
61
+ # patch summarize_chain (callable)
62
+ monkeypatch.setattr(
63
+ summarizer_mod,
64
+ "summarize_chain",
65
+ lambda *a, **kw: _MockResponse("Mocked summary"),
66
+ raising=False,
67
+ )
68
+
69
+ # 4) Patch ChatHuggingFace class in langchain_huggingface if available
70
+ lch_mod = try_import("langchain_huggingface")
71
+ if lch_mod is not None:
72
+ # Replace the ChatHuggingFace class with a callable factory that returns an object
73
+ # whose __call__ returns a MockResponse. Simpler: patch the class name itself to a
74
+ # lambda that returns our callable.
75
+ class DummyChat:
76
+ def __init__(self, *a, **kw):
77
+ pass
78
+
79
+ def __call__(self, *a, **kw):
80
+ return _MockResponse("Mocked generic LLM response")
81
+
82
+ monkeypatch.setattr(lch_mod, "ChatHuggingFace", DummyChat, raising=False)
83
+
84
+ # 5) As an extra safe fallback, if modules aren't importable, try to monkeypatch the
85
+ # dotted names but don't let import errors propagate (monkeypatch.setattr with strings
86
+ # can still try to import — we avoid that by only patching module objects above).
87
+ yield
tests/test_critic.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from graph_article.critic import critic_node
3
+
4
+ @pytest.mark.parametrize(
5
+ "mock_content,expected_abstract",
6
+ [
7
+ ("ACCEPTED", "A valid abstract"),
8
+ ("REJECTED", None),
9
+ ]
10
+ )
11
+ def test_critic_node(mock_content, expected_abstract, mocker):
12
+ class FakeResponse:
13
+ def __init__(self, content):
14
+ self.content = content
15
+
16
+ mock_chain = mocker.Mock()
17
+ mock_chain.invoke.return_value = FakeResponse(mock_content)
18
+
19
+ mocker.patch("graph_article.critic.critic_chain", mock_chain)
20
+
21
+ class DummyState:
22
+ abstract = "A valid abstract"
23
+
24
+ result = critic_node(DummyState())
25
+ assert result["critique"] == mock_content
26
+ assert result["final_abstract"] == expected_abstract
tests/test_graph_article.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from graph_article.graph_article import article_graph
2
+ from shared import ResearchState
3
+
4
+ class DummyResponse:
5
+ def __init__(self, content: str):
6
+ self.content = content
7
+
8
+
9
+ def test_article_graph_accept(mocker):
10
+ # Patch writer_chain.invoke to return an object with .content
11
+ mock_writer_chain = mocker.Mock()
12
+ mock_writer_chain.invoke.return_value = DummyResponse("Test abstract")
13
+ mocker.patch("graph_article.writer.writer_chain", mock_writer_chain)
14
+
15
+ # Patch critic_chain.invoke to return ACCEPTED (object with .content)
16
+ mock_critic_chain = mocker.Mock()
17
+ mock_critic_chain.invoke.return_value = DummyResponse("ACCEPTED")
18
+ mocker.patch("graph_article.critic.critic_chain", mock_critic_chain)
19
+
20
+ init_state = ResearchState(input="Title", category="Category")
21
+ final_state = article_graph.invoke(init_state)
22
+
23
+ assert final_state["final_abstract"] == "Test abstract"
24
+ assert final_state["critique"] == "ACCEPTED"
25
+
26
+
27
+ def test_article_graph_reject_then_accept(mocker):
28
+ # Patch writer_chain.invoke to return an object with .content
29
+ mock_writer_chain = mocker.Mock()
30
+ mock_writer_chain.invoke.return_value = DummyResponse("Test abstract")
31
+ mocker.patch("graph_article.writer.writer_chain", mock_writer_chain)
32
+
33
+ # Setup critic_chain.invoke to reject first (REJECTED), then accept (ACCEPTED)
34
+ call_count = {"count": 0}
35
+
36
+ def critic_invoke_side_effect(*args, **kwargs):
37
+ if call_count["count"] == 0:
38
+ call_count["count"] += 1
39
+ return DummyResponse("REJECTED")
40
+ return DummyResponse("ACCEPTED")
41
+
42
+ mock_critic_chain = mocker.Mock()
43
+ mock_critic_chain.invoke.side_effect = critic_invoke_side_effect
44
+ mocker.patch("graph_article.critic.critic_chain", mock_critic_chain)
45
+
46
+ init_state = ResearchState(input="Title", category="Category")
47
+ final_state = article_graph.invoke(init_state)
48
+
49
+ assert final_state["final_abstract"] == "Test abstract" or final_state["final_abstract"] == "Final abstract"
50
+ assert final_state["critique"] == "ACCEPTED"
tests/test_summarizer.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from graph_web.summarizer import summarize_node
2
+ from shared import ResearchState
3
+
4
+ def test_summarize_node_with_content(mocker):
5
+ class FakeResponse:
6
+ def __init__(self, content):
7
+ self.content = content
8
+
9
+ mock_chain = mocker.Mock()
10
+ mock_chain.invoke.return_value = FakeResponse("Summary of the content.")
11
+
12
+ mocker.patch("graph_web.summarizer.summarize_chain", mock_chain)
13
+
14
+ state = ResearchState(content="Some long content")
15
+ result = summarize_node(state)
16
+ assert result["summary"] == "Summary of the content."
17
+
18
+
19
+ def test_summarize_node_no_content():
20
+ state = ResearchState(content=None)
21
+ result = summarize_node(state)
22
+ assert result["summary"] == "No content to summarize"
tests/test_writer.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from graph_article.writer import writer_node
2
+
3
+ def test_writer_node_basic(mocker):
4
+ class FakeResponse:
5
+ def __init__(self, content):
6
+ self.content = content
7
+
8
+ mock_chain = mocker.Mock()
9
+ mock_chain.invoke.return_value = FakeResponse("This is a generated abstract.")
10
+
11
+ mocker.patch("graph_article.writer.writer_chain", mock_chain)
12
+
13
+ class DummyState:
14
+ input = "Test Title"
15
+ category = "Science"
16
+
17
+ result = writer_node(DummyState())
18
+ assert result["abstract"] == "This is a generated abstract."
utils/evaluation.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluation utilities for abstract quality.
3
+ """
4
+
5
+ def evaluate_abstract(abstract: str) -> dict:
6
+ """
7
+ Example heuristic evaluation: checks length and keyword presence.
8
+ """
9
+ word_count = len(abstract.split())
10
+ keywords = ["research", "method", "result", "conclusion"]
11
+ score = sum(1 for kw in keywords if kw in abstract.lower())
12
+
13
+ return {
14
+ "word_count": word_count,
15
+ "keyword_match_score": score,
16
+ "keywords_present": [kw for kw in keywords if kw in abstract.lower()]
17
+ }
utils/logger.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ def setup_logger(name=__name__):
4
+ logger = logging.getLogger(name)
5
+ logger.setLevel(logging.INFO)
6
+ if not logger.hasHandlers():
7
+ ch = logging.StreamHandler()
8
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
9
+ ch.setFormatter(formatter)
10
+ logger.addHandler(ch)
11
+ return logger
utils/retry.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import functools
3
+
4
+ def retry(exceptions, tries=3, delay=2, backoff=2):
5
+ """
6
+ Retry decorator with exponential backoff.
7
+
8
+ Args:
9
+ exceptions (tuple): exceptions to catch and retry on.
10
+ tries (int): number of attempts.
11
+ delay (int): initial delay between retries.
12
+ backoff (int): multiplier for delay.
13
+
14
+ Usage:
15
+ @retry((TimeoutError, SomeOtherError))
16
+ def func(...):
17
+ ...
18
+ """
19
+ def decorator(func):
20
+ @functools.wraps(func)
21
+ def wrapper(*args, **kwargs):
22
+ _tries, _delay = tries, delay
23
+ while _tries > 1:
24
+ try:
25
+ return func(*args, **kwargs)
26
+ except exceptions as e:
27
+ print(f"Warning: {e}, retrying in {_delay} seconds...")
28
+ time.sleep(_delay)
29
+ _tries -= 1
30
+ _delay *= backoff
31
+ return func(*args, **kwargs)
32
+ return wrapper
33
+ return decorator
utils/visualizer.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from IPython.display import Image, display
2
+ from pathlib import Path
3
+
4
+ def graph_visualiser(graph, filename: str = "graph.jpg", show: bool = True):
5
+ """
6
+ Visualize and save the compiled LangGraph as a .jpg image.
7
+
8
+ Args:
9
+ graph: Compiled LangGraph object.
10
+ filename (str): Path to save the image.
11
+ show (bool): Whether to display the image in notebook (optional).
12
+ """
13
+ try:
14
+ # Get raw image data
15
+ image_data = graph.get_graph().draw_mermaid_png()
16
+
17
+ # Save to file
18
+ output_path = Path(filename)
19
+ with open(output_path, "wb") as f:
20
+ f.write(image_data)
21
+
22
+ if show:
23
+ display(Image(filename))
24
+ print(f"✅ Graph image saved as: {output_path.resolve()}")
25
+
26
+ except Exception as e:
27
+ print(f"❌ Failed to generate graph image: {e}")
visuals/article_graph.jpg ADDED
visuals/web_graph.jpg ADDED