Skip to content

Commit 3ea4e41

Browse files
committed
improve citations and add object page
1 parent 35c68df commit 3ea4e41

File tree

14 files changed

+362
-138
lines changed

14 files changed

+362
-138
lines changed

app_home.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
11
import streamlit as st
22

3-
st.switch_page("pages/2_📒_Notebooks.py")
3+
from open_notebook.domain.base import ObjectModel
4+
from open_notebook.exceptions import NotFoundError
5+
from pages.components import (
6+
note_panel,
7+
source_embedding_panel,
8+
source_insight_panel,
9+
source_panel,
10+
)
11+
from pages.stream_app.utils import setup_page
12+
13+
setup_page("📒 Open Notebook", sidebar_state="collapsed")
14+
15+
if "object_id" not in st.query_params:
16+
st.switch_page("pages/2_📒_Notebooks.py")
17+
st.stop()
18+
19+
object_id = st.query_params["object_id"]
20+
try:
21+
obj = ObjectModel.get(object_id)
22+
except NotFoundError:
23+
st.switch_page("pages/2_📒_Notebooks.py")
24+
st.stop()
25+
26+
obj_type = object_id.split(":")[0]
27+
28+
if obj_type == "note":
29+
note_panel(object_id)
30+
elif obj_type == "source":
31+
source_panel(object_id)
32+
elif obj_type == "source_insight":
33+
source_insight_panel(object_id)
34+
elif obj_type == "source_embedding":
35+
source_embedding_panel(object_id)

open_notebook/domain/base.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar
2+
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, cast
33

44
from loguru import logger
55
from pydantic import BaseModel, ValidationError, field_validator
@@ -29,15 +29,26 @@ class ObjectModel(BaseModel):
2929
@classmethod
3030
def get_all(cls: Type[T], order_by=None) -> List[T]:
3131
try:
32+
# If called from a specific subclass, use its table_name
33+
if cls.table_name:
34+
target_class = cls
35+
table_name = cls.table_name
36+
else:
37+
# This path is taken if called directly from ObjectModel
38+
raise InvalidInputError(
39+
"get_all() must be called from a specific model class"
40+
)
41+
3242
if order_by:
3343
order = f" ORDER BY {order_by}"
3444
else:
3545
order = ""
36-
result = repo_query(f"SELECT * FROM {cls.table_name} {order}")
46+
47+
result = repo_query(f"SELECT * FROM {table_name} {order}")
3748
objects = []
3849
for obj in result:
3950
try:
40-
objects.append(cls(**obj))
51+
objects.append(target_class(**obj))
4152
except Exception as e:
4253
logger.critical(f"Error creating object: {str(e)}")
4354

@@ -52,15 +63,44 @@ def get(cls: Type[T], id: str) -> T:
5263
if not id:
5364
raise InvalidInputError("ID cannot be empty")
5465
try:
66+
# Get the table name from the ID (everything before the first colon)
67+
table_name = id.split(":")[0] if ":" in id else id
68+
69+
# If we're calling from a specific subclass and IDs match, use that class
70+
if cls.table_name and cls.table_name == table_name:
71+
target_class: Type[T] = cls
72+
else:
73+
# Otherwise, find the appropriate subclass based on table_name
74+
found_class = cls._get_class_by_table_name(table_name)
75+
if not found_class:
76+
raise InvalidInputError(f"No class found for table {table_name}")
77+
target_class = cast(Type[T], found_class)
78+
5579
result = repo_query(f"SELECT * FROM {id}")
5680
if result:
57-
return cls(**result[0])
81+
return target_class(**result[0])
5882
else:
59-
raise NotFoundError(f"{cls.table_name} with id {id} not found")
83+
raise NotFoundError(f"{table_name} with id {id} not found")
6084
except Exception as e:
61-
logger.error(f"Error fetching {cls.table_name} with id {id}: {str(e)}")
85+
logger.error(f"Error fetching object with id {id}: {str(e)}")
6286
logger.exception(e)
63-
raise NotFoundError(f"{cls.table_name} with id {id} not found")
87+
raise NotFoundError(f"Object with id {id} not found - {str(e)}")
88+
89+
@classmethod
90+
def _get_class_by_table_name(cls, table_name: str) -> Optional[Type["ObjectModel"]]:
91+
"""Find the appropriate subclass based on table_name."""
92+
93+
def get_all_subclasses(c: Type["ObjectModel"]) -> List[Type["ObjectModel"]]:
94+
all_subclasses: List[Type["ObjectModel"]] = []
95+
for subclass in c.__subclasses__():
96+
all_subclasses.append(subclass)
97+
all_subclasses.extend(get_all_subclasses(subclass))
98+
return all_subclasses
99+
100+
for subclass in get_all_subclasses(ObjectModel):
101+
if hasattr(subclass, "table_name") and subclass.table_name == table_name:
102+
return subclass
103+
return None
64104

65105
def needs_embedding(self) -> bool:
66106
return False

open_notebook/domain/notebook.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,42 @@ class Asset(BaseModel):
9393
url: Optional[str] = None
9494

9595

96+
class SourceEmbedding(ObjectModel):
97+
table_name: ClassVar[str] = "source_embedding"
98+
content: str
99+
100+
@property
101+
def source(self) -> "Source":
102+
try:
103+
src = repo_query(f"""
104+
select source.* from {self.id} fetch source
105+
106+
""")
107+
return Source(**src[0]["source"])
108+
except Exception as e:
109+
logger.error(f"Error fetching source for embedding {self.id}: {str(e)}")
110+
logger.exception(e)
111+
raise DatabaseOperationError(e)
112+
113+
96114
class SourceInsight(ObjectModel):
115+
table_name: ClassVar[str] = "source_insight"
97116
insight_type: str
98117
content: str
99118

119+
@property
120+
def source(self) -> "Source":
121+
try:
122+
src = repo_query(f"""
123+
select source.* from {self.id} fetch source
124+
125+
""")
126+
return Source(**src[0]["source"])
127+
except Exception as e:
128+
logger.error(f"Error fetching source for insight {self.id}: {str(e)}")
129+
logger.exception(e)
130+
raise DatabaseOperationError(e)
131+
100132

101133
class Source(ObjectModel):
102134
table_name: ClassVar[str] = "source"
@@ -112,7 +144,7 @@ def get_context(
112144
return dict(
113145
id=self.id,
114146
title=self.title,
115-
insights=self.insights,
147+
insights=[insight.model_dump() for insight in self.insights],
116148
full_text=self.full_text,
117149
)
118150
else:

pages/3_🔍_Ask_and_Search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from open_notebook.domain.models import Model
44
from open_notebook.domain.notebook import text_search, vector_search
55
from open_notebook.graphs.rag import graph as rag_graph
6-
from pages.stream_app.utils import setup_page
6+
from pages.stream_app.utils import convert_source_references, setup_page
77

88
setup_page("🔍 Search")
99

@@ -40,7 +40,7 @@ def results_card(item):
4040
messages=messages
4141
), # config=dict(configurable=dict(model_id=model.id))
4242
)
43-
st.markdown(rag_results["messages"][-1].content)
43+
st.markdown(convert_source_references(rag_results["messages"][-1].content))
4444
with st.expander("Details (for debugging)"):
4545
st.json(rag_results)
4646

pages/components/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pages.components.note_panel import note_panel
2+
from pages.components.source_embedding_panel import source_embedding_panel
3+
from pages.components.source_insight import source_insight_panel
4+
from pages.components.source_panel import source_panel
5+
6+
__all__ = [
7+
"note_panel",
8+
"source_embedding_panel",
9+
"source_insight_panel",
10+
"source_panel",
11+
]

pages/components/note_panel.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import streamlit as st
2+
from loguru import logger
3+
from streamlit_monaco import st_monaco # type: ignore
4+
5+
from open_notebook.domain.notebook import Note
6+
7+
8+
def note_panel(note_id, notebook_id=None):
9+
note: Note = Note.get(note_id)
10+
if not note:
11+
raise ValueError(f"Note not fonud {note_id}")
12+
t_preview, t_edit = st.tabs(["Preview", "Edit"])
13+
with t_preview:
14+
st.subheader(note.title)
15+
st.markdown(note.content)
16+
with t_edit:
17+
note.title = st.text_input("Title", value=note.title)
18+
note.content = st_monaco(
19+
value=note.content, height="600px", language="markdown"
20+
)
21+
if st.button("Save", key=f"pn_edit_note_{note.id or 'new'}"):
22+
logger.debug("Editing note")
23+
note.save()
24+
if not note.id and notebook_id:
25+
note.add_to_notebook(notebook_id)
26+
st.rerun()
27+
if st.button("Delete", type="primary", key=f"delete_note_{note.id or 'new'}"):
28+
logger.debug("Deleting note")
29+
note.delete()
30+
st.rerun()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import streamlit as st
2+
3+
from open_notebook.domain.notebook import SourceEmbedding
4+
5+
6+
def source_embedding_panel(source_embedding_id):
7+
si: SourceEmbedding = SourceEmbedding.get(source_embedding_id)
8+
if not si:
9+
raise ValueError(f"Embedding not found {source_embedding_id}")
10+
with st.container(border=True):
11+
url = f"Navigator?object_id={si.source.id}"
12+
st.markdown("**Original Source**")
13+
st.markdown(f"{si.source.title} [link](%s)" % url)
14+
st.markdown(si.content)
15+
if st.button("Delete", type="primary", key=f"delete_embedding_{si.id or 'new'}"):
16+
si.delete()
17+
st.rerun()

pages/components/source_insight.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import streamlit as st
2+
3+
from open_notebook.domain.notebook import SourceInsight
4+
5+
6+
def source_insight_panel(source, notebook_id=None):
7+
si: SourceInsight = SourceInsight.get(source)
8+
if not si:
9+
raise ValueError(f"insight not found {source}")
10+
st.subheader(si.insight_type)
11+
with st.container(border=True):
12+
url = f"Navigator?object_id={si.source.id}"
13+
st.markdown("**Original Source**")
14+
st.markdown(f"{si.source.title} [link](%s)" % url)
15+
st.markdown(si.content)
16+
if st.button("Delete", type="primary", key=f"delete_insight_{si.id or 'new'}"):
17+
si.delete()
18+
st.rerun()

pages/components/source_panel.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import streamlit as st
2+
import streamlit_scrollable_textbox as stx # type: ignore
3+
import yaml
4+
from humanize import naturaltime
5+
6+
from open_notebook.domain.notebook import Source
7+
from open_notebook.utils import surreal_clean
8+
from pages.stream_app.utils import run_patterns
9+
10+
11+
def source_panel(source_id: str, modal=False):
12+
source: Source = Source.get(source_id)
13+
if not source:
14+
raise ValueError(f"Source not found: {source_id}")
15+
16+
current_title = source.title if source.title else "No Title"
17+
source.title = st.text_input("Title", value=current_title)
18+
if source.title != current_title:
19+
st.toast("Saved new Title")
20+
source.save()
21+
22+
process_tab, source_tab = st.tabs(["Process", "Source"])
23+
with process_tab:
24+
c1, c2 = st.columns([3, 1])
25+
with c1:
26+
title = st.empty()
27+
if source.title:
28+
title.subheader(source.title)
29+
if source.asset and source.asset.url:
30+
from_src = f"from URL: {source.asset.url}"
31+
elif source.asset and source.asset.file_path:
32+
from_src = f"from file: {source.asset.file_path}"
33+
else:
34+
from_src = "from text"
35+
st.caption(f"Created {naturaltime(source.created)}, {from_src}")
36+
for insight in source.insights:
37+
with st.expander(f"**{insight.insight_type}**"):
38+
st.markdown(insight.content)
39+
if st.button(
40+
"Delete", type="primary", key=f"delete_insight_{insight.id}"
41+
):
42+
insight.delete()
43+
st.rerun(scope="fragment" if modal else "app")
44+
45+
with c2:
46+
with open("transformations.yaml", "r") as file:
47+
transformations = yaml.safe_load(file)
48+
for transformation in transformations["source_insights"]:
49+
if st.button(
50+
transformation["name"], help=transformation["description"]
51+
):
52+
result = run_patterns(
53+
source.full_text, transformation["patterns"]
54+
)
55+
source.add_insight(
56+
transformation["insight_type"], surreal_clean(result)
57+
)
58+
st.rerun(scope="fragment" if modal else "app")
59+
60+
if st.button(
61+
"Embed vectors",
62+
icon="🦾",
63+
disabled=source.embedded_chunks > 0,
64+
help="This will generate your embedding vectors on the database for powerful search capabilities",
65+
):
66+
source.vectorize()
67+
st.success("Embedding complete")
68+
69+
chk_delete = st.checkbox(
70+
"🗑️ Delete source", key=f"delete_source_{source.id}", value=False
71+
)
72+
if chk_delete:
73+
st.warning(
74+
"Source will be deleted with all its insights and embeddings"
75+
)
76+
if st.button(
77+
"Delete", type="primary", key=f"bt_delete_source_{source.id}"
78+
):
79+
source.delete()
80+
st.rerun()
81+
82+
with source_tab:
83+
st.subheader("Content")
84+
stx.scrollableTextbox(source.full_text, height=300)

0 commit comments

Comments
 (0)