separate source and content graph

This commit is contained in:
LUIS NOVO 2024-11-10 13:30:03 -03:00
parent b42a95b35f
commit 2e2a4947b3
12 changed files with 167 additions and 56 deletions

View file

@ -0,0 +1,19 @@
from typing import ClassVar, List, Optional
import yaml
from pydantic import Field
from open_notebook.domain.base import RecordModel
class Transformation:
@classmethod
def get_all(cls):
with open("transformations.yaml", "r") as file:
transformations = yaml.safe_load(file)
return transformations
class DefaultTransformations(RecordModel):
record_id: ClassVar[str] = "open_notebook:default_transformations"
source_insights: Optional[List[str]] = Field(default_factory=list)

View file

@ -14,14 +14,14 @@ from open_notebook.graphs.content_processing.pdf import (
SUPPORTED_FITZ_TYPES, SUPPORTED_FITZ_TYPES,
extract_pdf, extract_pdf,
) )
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
from open_notebook.graphs.content_processing.text import extract_txt from open_notebook.graphs.content_processing.text import extract_txt
from open_notebook.graphs.content_processing.url import extract_url, url_provider from open_notebook.graphs.content_processing.url import extract_url, url_provider
from open_notebook.graphs.content_processing.video import extract_best_audio_from_video from open_notebook.graphs.content_processing.video import extract_best_audio_from_video
from open_notebook.graphs.content_processing.youtube import extract_youtube_transcript from open_notebook.graphs.content_processing.youtube import extract_youtube_transcript
def source_identification(state: SourceState): def source_identification(state: ContentState):
""" """
Identify the content source based on parameters Identify the content source based on parameters
""" """
@ -37,7 +37,7 @@ def source_identification(state: SourceState):
return {"source_type": doc_type} return {"source_type": doc_type}
def file_type(state: SourceState): def file_type(state: ContentState):
""" """
Identify the file using python-magic Identify the file using python-magic
""" """
@ -45,10 +45,11 @@ def file_type(state: SourceState):
file_path = state.get("file_path") file_path = state.get("file_path")
if file_path is not None: if file_path is not None:
return_dict["identified_type"] = magic.from_file(file_path, mime=True) return_dict["identified_type"] = magic.from_file(file_path, mime=True)
return_dict["title"] = os.path.basename(file_path)
return return_dict return return_dict
def file_type_edge(data: SourceState): def file_type_edge(data: ContentState):
assert data.get("identified_type"), "Type not identified" assert data.get("identified_type"), "Type not identified"
identified_type = data["identified_type"] identified_type = data["identified_type"]
@ -68,7 +69,7 @@ def file_type_edge(data: SourceState):
) )
def delete_file(data: SourceState): def delete_file(data: ContentState):
if data.get("delete_source"): if data.get("delete_source"):
logger.debug(f"Deleting file: {data.get('file_path')}") logger.debug(f"Deleting file: {data.get('file_path')}")
file_path = data.get("file_path") file_path = data.get("file_path")
@ -82,7 +83,7 @@ def delete_file(data: SourceState):
logger.debug("Not deleting file") logger.debug("Not deleting file")
workflow = StateGraph(SourceState) workflow = StateGraph(ContentState)
workflow.add_node("source", source_identification) workflow.add_node("source", source_identification)
workflow.add_node("url_provider", url_provider) workflow.add_node("url_provider", url_provider)
workflow.add_node("file_type", file_type) workflow.add_node("file_type", file_type)

View file

@ -5,7 +5,7 @@ from loguru import logger
from pydub import AudioSegment from pydub import AudioSegment
from open_notebook.domain.models import model_manager from open_notebook.domain.models import model_manager
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
# todo: remove reference to model_manager # todo: remove reference to model_manager
# future: parallelize the transcription process # future: parallelize the transcription process
@ -72,7 +72,7 @@ def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
return output_files return output_files
def extract_audio(data: SourceState): def extract_audio(data: ContentState):
SPEECH_TO_TEXT_MODEL = model_manager.speech_to_text SPEECH_TO_TEXT_MODEL = model_manager.speech_to_text
input_audio_path = data.get("file_path") input_audio_path = data.get("file_path")

View file

@ -3,7 +3,7 @@ from loguru import logger
from openpyxl import load_workbook from openpyxl import load_workbook
from pptx import Presentation from pptx import Presentation
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
SUPPORTED_OFFICE_TYPES = [ SUPPORTED_OFFICE_TYPES = [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@ -251,7 +251,7 @@ def get_xlsx_info(file_path):
return None return None
def extract_office_content(state: SourceState): def extract_office_content(state: ContentState):
"""Universal function to extract content from Office files""" """Universal function to extract content from Office files"""
assert state.get("file_path"), "No file path provided" assert state.get("file_path"), "No file path provided"
assert ( assert (

View file

@ -4,7 +4,7 @@ import unicodedata
import fitz # type: ignore import fitz # type: ignore
from loguru import logger from loguru import logger
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
# todo: find tables - https://pymupdf.readthedocs.io/en/latest/the-basics.html#extracting-tables-from-a-page # todo: find tables - https://pymupdf.readthedocs.io/en/latest/the-basics.html#extracting-tables-from-a-page
# todo: what else can we do to make the text more readable? # todo: what else can we do to make the text more readable?
@ -127,7 +127,7 @@ def _extract_text_from_pdf(pdf_path):
doc.close() doc.close()
def extract_pdf(state: SourceState): def extract_pdf(state: ContentState):
""" """
Parse the text file and print its content. Parse the text file and print its content.
""" """

View file

@ -1,7 +1,7 @@
from typing_extensions import TypedDict from typing_extensions import TypedDict
class SourceState(TypedDict): class ContentState(TypedDict):
content: str content: str
file_path: str file_path: str
url: str url: str

View file

@ -1,9 +1,9 @@
from loguru import logger from loguru import logger
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
def extract_txt(state: SourceState): def extract_txt(state: ContentState):
""" """
Parse the text file and print its content. Parse the text file and print its content.
""" """

View file

@ -5,14 +5,14 @@ import requests # type: ignore
from bs4 import BeautifulSoup, Comment from bs4 import BeautifulSoup, Comment
from loguru import logger from loguru import logger
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
# future: better extraction methods # future: better extraction methods
# https://github.com/buriy/python-readability # https://github.com/buriy/python-readability
# also try readability: from readability import Document # also try readability: from readability import Document
def url_provider(state: SourceState): def url_provider(state: ContentState):
""" """
Identify the provider Identify the provider
""" """
@ -173,7 +173,7 @@ def extract_url_jina(url: str):
return {"content": text} return {"content": text}
def extract_url(state: SourceState): def extract_url(state: ContentState):
assert state.get("url"), "No URL provided" assert state.get("url"), "No URL provided"
url = state["url"] url = state["url"]
try: try:

View file

@ -4,7 +4,7 @@ import subprocess
from loguru import logger from loguru import logger
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
def extract_audio_from_video(input_file, output_file, stream_index): def extract_audio_from_video(input_file, output_file, stream_index):
@ -102,7 +102,7 @@ def select_best_audio_stream(streams):
return max(scored_streams, key=lambda x: x[0])[1] return max(scored_streams, key=lambda x: x[0])[1]
def extract_best_audio_from_video(data: SourceState): def extract_best_audio_from_video(data: ContentState):
""" """
Main function to extract the best audio stream from a video file Main function to extract the best audio stream from a video file
""" """

View file

@ -9,7 +9,7 @@ from youtube_transcript_api.formatters import TextFormatter # type: ignore
from open_notebook.config import CONFIG from open_notebook.config import CONFIG
from open_notebook.exceptions import NoTranscriptFound from open_notebook.exceptions import NoTranscriptFound
from open_notebook.graphs.content_processing.state import SourceState from open_notebook.graphs.content_processing.state import ContentState
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
@ -129,7 +129,7 @@ def get_best_transcript(video_id, preferred_langs=["en", "es", "pt"]):
return None return None
def extract_youtube_transcript(state: SourceState): def extract_youtube_transcript(state: ContentState):
""" """
Parse the text file and print its content. Parse the text file and print its content.
""" """

View file

@ -0,0 +1,106 @@
import operator
from typing import List
from langchain_core.runnables import (
RunnableConfig,
)
from langgraph.graph import END, START, StateGraph
from langgraph.types import Send
from loguru import logger
from typing_extensions import Annotated, TypedDict
from open_notebook.domain.notebook import Asset, Source
from open_notebook.domain.transformation import Transformation
from open_notebook.graphs.content_processing import ContentState
from open_notebook.graphs.content_processing import graph as content_graph
from open_notebook.graphs.multipattern import graph as transform_graph
from open_notebook.utils import surreal_clean
# todo: we can make this more efficient
class SourceState(TypedDict):
content_state: ContentState
transformations: List[str]
notebook_id: str
source: Source
transformations: Annotated[list, operator.add]
class TransformationState(TypedDict):
source: Source
transformation: dict
def content_process(state: SourceState):
content_state = state["content_state"]
logger.debug("Content processing started for new content")
return {"content_state": content_graph.invoke(content_state)}
def run_patterns(input_text, patterns):
output = transform_graph.invoke(dict(content_stack=[input_text], patterns=patterns))
return output["output"]
def save_source(state: SourceState):
logger.debug("Saving source")
content_state = state["content_state"]
source = Source(
asset=Asset(
url=content_state.get("url"), file_path=content_state.get("file_path")
),
full_text=surreal_clean(content_state["content"]),
title=content_state.get("title"),
)
source.save()
if state["notebook_id"]:
logger.debug(f"Adding source to notebook {state['notebook_id']}")
source.add_to_notebook(state["notebook_id"])
return {"source": source}
def trigger_transformations(state: SourceState, config: RunnableConfig):
if len(state["transformations"]) == 0:
return []
transformations = Transformation.get_all()
to_apply = [
t
for t in transformations["source_insights"]
if t["name"] in state["transformations"]
]
logger.debug(f"Applying transformations {to_apply}")
return [
Send(
"transform_content",
{
"source": state["source"],
"transformation": t,
},
)
for t in to_apply
]
def transform_content(state: TransformationState):
source = state["source"]
content = source.full_text
transformation = state["transformation"]
logger.debug(f"Applying transformation {transformation['name']}")
result = run_patterns(content, patterns=transformation["patterns"])
source.add_insight(transformation["name"], surreal_clean(result))
return {"transformations": [{"name": transformation["name"], "content": result}]}
workflow = StateGraph(SourceState)
workflow.add_node("content_process", content_process)
workflow.add_node("save_source", save_source)
workflow.add_node("transform_content", transform_content)
workflow.add_edge(START, "content_process")
workflow.add_edge("content_process", "save_source")
workflow.add_conditional_edges(
"save_source", trigger_transformations, ["transform_content"]
)
workflow.add_edge("transform_content", END)
source_graph = workflow.compile()

View file

@ -6,36 +6,15 @@ from humanize import naturaltime
from loguru import logger from loguru import logger
from open_notebook.config import UPLOADS_FOLDER from open_notebook.config import UPLOADS_FOLDER
from open_notebook.domain.notebook import Asset, Source from open_notebook.domain.notebook import Source
from open_notebook.domain.transformation import DefaultTransformations, Transformation
from open_notebook.exceptions import UnsupportedTypeException from open_notebook.exceptions import UnsupportedTypeException
from open_notebook.graphs.content_processing import graph from open_notebook.graphs.source import source_graph
from open_notebook.utils import surreal_clean
from pages.components import source_panel from pages.components import source_panel
from pages.stream_app.utils import run_patterns
from .consts import context_icons from .consts import context_icons
# moved it here to replace it with the pipeline on 0.1.0
def generate_toc_and_title(source) -> "Source":
try:
patterns = ["patterns/default/toc"]
result = run_patterns(source.full_text, patterns=patterns)
source.add_insight("Table of Contents", surreal_clean(result))
if not source.title:
patterns = [
"Based on the Table of Contents below, please provide a Title for this content, with max 15 words"
]
output = run_patterns(result, patterns=patterns)
source.title = surreal_clean(output)
source.save()
return source
except Exception as e:
logger.error(f"Error summarizing source {source.id}: {str(e)}")
logger.exception(e)
raise
@st.dialog("Source", width="large") @st.dialog("Source", width="large")
def source_panel_dialog(source_id): def source_panel_dialog(source_id):
source_panel(source_id, modal=True) source_panel(source_id, modal=True)
@ -48,6 +27,7 @@ def add_source(notebook_id):
source_text = None source_text = None
source_type = st.radio("Type", ["Link", "Upload", "Text"]) source_type = st.radio("Type", ["Link", "Upload", "Text"])
req = {} req = {}
transformations = Transformation.get_all()
if source_type == "Link": if source_type == "Link":
source_link = st.text_input("Link") source_link = st.text_input("Link")
req["url"] = source_link req["url"] = source_link
@ -58,6 +38,14 @@ def add_source(notebook_id):
else: else:
source_text = st.text_area("Text") source_text = st.text_area("Text")
req["content"] = source_text req["content"] = source_text
default_transformations = [t for t in DefaultTransformations().source_insights]
available_transformations = [t["name"] for t in transformations["source_insights"]]
apply_transformations = st.multiselect(
"Apply transformations",
options=available_transformations,
default=default_transformations,
)
if st.button("Process", key="add_source"): if st.button("Process", key="add_source"):
logger.debug("Adding source") logger.debug("Adding source")
with st.status("Processing...", expanded=True): with st.status("Processing...", expanded=True):
@ -82,17 +70,14 @@ def add_source(notebook_id):
with open(new_path, "wb") as f: with open(new_path, "wb") as f:
f.write(source_file.getbuffer()) f.write(source_file.getbuffer())
result = graph.invoke(req) st.write("Processing content..")
st.write("Saving..") source_graph.invoke(
source = Source( {
asset=Asset(url=req.get("url"), file_path=req.get("file_path")), "content_state": req,
full_text=surreal_clean(result["content"]), "notebook_id": notebook_id,
title=result.get("title"), "transformations": apply_transformations,
}
) )
source.save()
source.add_to_notebook(notebook_id)
st.write("Summarizing...")
generate_toc_and_title(source)
except UnsupportedTypeException as e: except UnsupportedTypeException as e:
st.warning( st.warning(
"This type of content is not supported yet. If you think it should be, let us know on the project Issues's page" "This type of content is not supported yet. If you think it should be, let us know on the project Issues's page"