commit
34c3b6421a
35 changed files with 807 additions and 723 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
prompts/patterns/user/
|
||||||
notebooks/
|
notebooks/
|
||||||
data/
|
data/
|
||||||
.uploads/
|
.uploads/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ In a world dominated by Artificial Intelligence, having the ability to think
|
||||||
|
|
||||||
Open Notebook empowers you to manage your research, generate AI-assisted notes, and interact with your content—on your terms.
|
Open Notebook empowers you to manage your research, generate AI-assisted notes, and interact with your content—on your terms.
|
||||||
|
|
||||||
|
Learn more about our project at [https://www.open-notebook.ai](https://www.open-notebook.ai)
|
||||||
|
|
||||||
## ⚙️ Setting Up
|
## ⚙️ Setting Up
|
||||||
|
|
||||||
Go to the [Setup Guide](docs/SETUP.md) to learn how to set up the tool in details.
|
Go to the [Setup Guide](docs/SETUP.md) to learn how to set up the tool in details.
|
||||||
|
|
|
||||||
|
|
@ -41,44 +41,7 @@ volumes:
|
||||||
notebook_data:
|
notebook_data:
|
||||||
```
|
```
|
||||||
|
|
||||||
or with the environment variables:
|
Take a look at the [Open Notebook Boilerplate](https://github.com/lfnovo/open-notebook-boilerplate) repo with a sample of how to set it up for maximum feature usability.
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
surrealdb:
|
|
||||||
image: surrealdb/surrealdb:v2
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
volumes:
|
|
||||||
- surreal_data:/mydata
|
|
||||||
command: start --log trace --user root --pass root rocksdb:/mydata/mydatabase.db
|
|
||||||
pull_policy: always
|
|
||||||
user: root
|
|
||||||
|
|
||||||
open_notebook:
|
|
||||||
image: lfnovo/open_notebook:latest
|
|
||||||
ports:
|
|
||||||
- "8080:8502"
|
|
||||||
environment:
|
|
||||||
- OPENAI_API_KEY=API_KEY
|
|
||||||
- SURREAL_ADDRESSsurrealdb
|
|
||||||
- SURREAL_PORT=8000
|
|
||||||
- SURREAL_USER=root
|
|
||||||
- SURREAL_PASS=root
|
|
||||||
- SURREAL_NAMESPACE=open_notebook
|
|
||||||
- SURREAL_DATABASE=staging
|
|
||||||
depends_on:
|
|
||||||
- surrealdb
|
|
||||||
pull_policy: always
|
|
||||||
volumes:
|
|
||||||
- notebook_data:/app/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
surreal_data:
|
|
||||||
notebook_data:
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📦 Installing from Source
|
### 📦 Installing from Source
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ For example, you could start by summarizing a text, then use that summary to gen
|
||||||
|
|
||||||
### Setting Up Transformations
|
### Setting Up Transformations
|
||||||
|
|
||||||
|
Take a look at the [Open Notebook Boilerplate](https://github.com/lfnovo/open-notebook-boilerplate) repo with a sample of how to set it up for maximum feature usability.
|
||||||
|
|
||||||
To set up your own Transformations, you'll define them in the `transformations.yaml` file. Below is an example setup:
|
To set up your own Transformations, you'll define them in the `transformations.yaml` file. Below is an example setup:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -31,35 +33,38 @@ source_insights:
|
||||||
- name: "Summarize"
|
- name: "Summarize"
|
||||||
insight_type: "Content Summary"
|
insight_type: "Content Summary"
|
||||||
description: "Summarize the content"
|
description: "Summarize the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/makeitdense
|
- patterns/default/makeitdense
|
||||||
- patterns/summarize
|
- patterns/default/summarize
|
||||||
- name: "Key Insights"
|
- name: "Key Insights"
|
||||||
insight_type: "Key Insights"
|
insight_type: "Key Insights"
|
||||||
description: "Extracts a list of the Key Insights of the content"
|
description: "Extracts a list of the Key Insights of the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/keyinsights
|
- patterns/default/keyinsights
|
||||||
- name: "Make it Dense"
|
- name: "Make it Dense"
|
||||||
insight_type: "Dense Representation"
|
insight_type: "Dense Representation"
|
||||||
description: "Create a dense representation of the content"
|
description: "Create a dense representation of the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/makeitdense
|
- patterns/default/makeitdense
|
||||||
- name: "Analyze Paper"
|
- name: "Analyze Paper"
|
||||||
insight_type: "Paper Analysis"
|
insight_type: "Paper Analysis"
|
||||||
description: "Analyze the paper and provide a quick summary"
|
description: "Analyze the paper and provide a quick summary"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/analyze_paper
|
- patterns/default/analyze_paper
|
||||||
- name: "Reflection"
|
- name: "Reflection"
|
||||||
insight_type: "Reflection Questions"
|
insight_type: "Reflection Questions"
|
||||||
description: "Generates a list of insightful questions to provoke reflection"
|
description: "Generates a list of insightful questions to provoke reflection"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/reflection_questions
|
- patterns/default/reflection_questions
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you've defined your transformation, make sure to add the corresponding prompts to the `prompts/patterns` folder. Here's an example of a transformation prompt:
|
You can mount this file to the docker image to replace its default value.
|
||||||
|
|
||||||
|
|
||||||
|
Once you've defined your transformation, make sure to add the corresponding prompts to the `prompts/patterns/user` folder. Here's an example of a transformation prompt:
|
||||||
|
|
||||||
```jinja
|
```jinja
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/user/common_text.jinja' %}
|
||||||
|
|
||||||
# IDENTITY and PURPOSE
|
# IDENTITY and PURPOSE
|
||||||
|
|
||||||
|
|
@ -95,7 +100,6 @@ You extract deep, thought-provoking, and meaningful reflections from text conten
|
||||||
- Any item that doesn't follow the `patterns/` format will be interpreted as a command (refer to `command.jinja` for clarity).
|
- Any item that doesn't follow the `patterns/` format will be interpreted as a command (refer to `command.jinja` for clarity).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Call for Contributions
|
### Call for Contributions
|
||||||
|
|
||||||
Have an idea for an amazing Transformation? We'd love to see your creativity! Please submit a pull request with your favorite transformations to help expand our library. Whether it's summarization, content analysis, or something entirely unique, your contributions will help us all get more out of our research!Leveraging Transformations in Open Notebook
|
Have an idea for an amazing Transformation? We'd love to see your creativity! Please submit a pull request with your favorite transformations to help expand our library. Whether it's summarization, content analysis, or something entirely unique, your contributions will help us all get more out of our research!Leveraging Transformations in Open Notebook
|
||||||
|
|
|
||||||
|
|
@ -36,18 +36,20 @@ PODCASTS_FOLDER = f"{DATA_FOLDER}/podcasts"
|
||||||
os.makedirs(PODCASTS_FOLDER, exist_ok=True)
|
os.makedirs(PODCASTS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_MODELS = DefaultModels.load()
|
def load_default_models():
|
||||||
|
default_models = DefaultModels.load()
|
||||||
if DEFAULT_MODELS.default_embedding_model:
|
embedding_model = (
|
||||||
EMBEDDING_MODEL = get_model(
|
get_model(default_models.default_embedding_model, model_type="embedding")
|
||||||
DEFAULT_MODELS.default_embedding_model, model_type="embedding"
|
if default_models.default_embedding_model
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
EMBEDDING_MODEL = None
|
|
||||||
|
|
||||||
if DEFAULT_MODELS.default_speech_to_text_model:
|
speech_to_text_model = (
|
||||||
SPEECH_TO_TEXT_MODEL = get_model(
|
get_model(
|
||||||
DEFAULT_MODELS.default_speech_to_text_model, model_type="speech_to_text"
|
default_models.default_speech_to_text_model, model_type="speech_to_text"
|
||||||
|
)
|
||||||
|
if default_models.default_speech_to_text_model
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
SPEECH_TO_TEXT_MODEL = None
|
return default_models, embedding_model, speech_to_text_model
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,15 @@ class ObjectModel(BaseModel):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
from open_notebook.config import EMBEDDING_MODEL
|
from open_notebook.config import load_default_models
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Validating {self.__class__.__name__}")
|
logger.debug(f"Validating {self.__class__.__name__}")
|
||||||
self.model_validate(self.model_dump(), strict=True)
|
self.model_validate(self.model_dump(), strict=True)
|
||||||
data = self._prepare_save_data()
|
data = self._prepare_save_data()
|
||||||
data["updated"] = datetime.now().isoformat()
|
data["updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
if self.needs_embedding():
|
if self.needs_embedding():
|
||||||
embedding_content = self.get_embedding_content()
|
embedding_content = self.get_embedding_content()
|
||||||
|
|
@ -82,10 +84,11 @@ class ObjectModel(BaseModel):
|
||||||
data["embedding"] = EMBEDDING_MODEL.embed(embedding_content)
|
data["embedding"] = EMBEDDING_MODEL.embed(embedding_content)
|
||||||
|
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
data["created"] = datetime.now().isoformat()
|
data["created"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
logger.debug("Creating new record")
|
logger.debug("Creating new record")
|
||||||
repo_result = repo_create(self.__class__.table_name, data)
|
repo_result = repo_create(self.__class__.table_name, data)
|
||||||
else:
|
else:
|
||||||
|
data["created"] = self.created.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
logger.debug(f"Updating record with id {self.id}")
|
logger.debug(f"Updating record with id {self.id}")
|
||||||
repo_result = repo_update(self.id, data)
|
repo_result = repo_update(self.id, data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.runnables.config import RunnableConfig
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
from open_notebook.config import EMBEDDING_MODEL
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.database.repository import (
|
from open_notebook.database.repository import (
|
||||||
repo_create,
|
repo_create,
|
||||||
repo_query,
|
repo_query,
|
||||||
|
|
@ -140,6 +140,8 @@ class Source(ObjectModel):
|
||||||
raise DatabaseOperationError(e)
|
raise DatabaseOperationError(e)
|
||||||
|
|
||||||
def vectorize(self) -> None:
|
def vectorize(self) -> None:
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.full_text:
|
if not self.full_text:
|
||||||
return
|
return
|
||||||
|
|
@ -189,6 +191,8 @@ class Source(ObjectModel):
|
||||||
raise DatabaseOperationError("Failed to search sources")
|
raise DatabaseOperationError("Failed to search sources")
|
||||||
|
|
||||||
def add_insight(self, insight_type: str, content: str) -> Any:
|
def add_insight(self, insight_type: str, content: str) -> Any:
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
if not insight_type or not content:
|
if not insight_type or not content:
|
||||||
raise InvalidInputError("Insight type and content must be provided")
|
raise InvalidInputError("Insight type and content must be provided")
|
||||||
try:
|
try:
|
||||||
|
|
@ -209,6 +213,8 @@ class Source(ObjectModel):
|
||||||
|
|
||||||
# todo: move this to content processing pipeline as a major graph
|
# todo: move this to content processing pipeline as a major graph
|
||||||
def generate_toc_and_title(self) -> "Source":
|
def generate_toc_and_title(self) -> "Source":
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = RunnableConfig(configurable=dict(thread_id=self.id))
|
config = RunnableConfig(configurable=dict(thread_id=self.id))
|
||||||
result = toc_graph.invoke({"content": self.full_text}, config=config)
|
result = toc_graph.invoke({"content": self.full_text}, config=config)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ from langgraph.graph import END, START, StateGraph
|
||||||
from langgraph.graph.message import add_messages
|
from langgraph.graph.message import add_messages
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS, LANGGRAPH_CHECKPOINT_FILE
|
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE, load_default_models
|
||||||
from open_notebook.domain.notebook import Notebook
|
from open_notebook.domain.notebook import Notebook
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class ThreadState(TypedDict):
|
class ThreadState(TypedDict):
|
||||||
messages: Annotated[list, add_messages]
|
messages: Annotated[list, add_messages]
|
||||||
|
|
@ -22,12 +24,12 @@ class ThreadState(TypedDict):
|
||||||
|
|
||||||
|
|
||||||
def call_model_with_messages(state: ThreadState, config: RunnableConfig) -> dict:
|
def call_model_with_messages(state: ThreadState, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", DEFAULT_MODELS.default_chat_model
|
"model_id", DEFAULT_MODELS.default_chat_model
|
||||||
)
|
)
|
||||||
ai_message = run_pattern(
|
ai_message = run_pattern(
|
||||||
"chat",
|
"chat",
|
||||||
model_name,
|
model_id,
|
||||||
messages=state["messages"],
|
messages=state["messages"],
|
||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from math import ceil
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
|
|
||||||
from open_notebook.config import SPEECH_TO_TEXT_MODEL
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.graphs.content_processing.state import SourceState
|
from open_notebook.graphs.content_processing.state import SourceState
|
||||||
|
|
||||||
# future: parallelize the transcription process
|
# future: parallelize the transcription process
|
||||||
|
|
@ -72,6 +72,8 @@ def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
|
||||||
|
|
||||||
|
|
||||||
def extract_audio(data: SourceState):
|
def extract_audio(data: SourceState):
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
input_audio_path = data.get("file_path")
|
input_audio_path = data.get("file_path")
|
||||||
audio_files = []
|
audio_files = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from langchain_core.runnables import (
|
from langchain_core.runnables import (
|
||||||
RunnableConfig,
|
RunnableConfig,
|
||||||
)
|
)
|
||||||
from langgraph.graph import END, START, StateGraph
|
from langgraph.graph import END, START, StateGraph
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.domain.notebook import Note, Notebook, Source
|
from open_notebook.domain.notebook import Note, Notebook, Source
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class DocQueryState(TypedDict):
|
class DocQueryState(TypedDict):
|
||||||
doc_id: str
|
doc_id: str
|
||||||
|
|
@ -19,10 +20,10 @@ class DocQueryState(TypedDict):
|
||||||
|
|
||||||
|
|
||||||
def call_model(state: dict, config: RunnableConfig) -> dict:
|
def call_model(state: dict, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", os.environ.get("RETRIEVAL_MODEL")
|
"model_id", DEFAULT_MODELS.default_transformation_model
|
||||||
)
|
)
|
||||||
return {"answer": run_pattern("doc_query", model_name, state)}
|
return {"answer": run_pattern("doc_query", model_id, state)}
|
||||||
|
|
||||||
|
|
||||||
# todo: there is probably a better way to do this and avoid repetition
|
# todo: there is probably a better way to do this and avoid repetition
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,24 @@ from langchain_core.runnables import (
|
||||||
from langgraph.graph import END, START, StateGraph
|
from langgraph.graph import END, START, StateGraph
|
||||||
from typing_extensions import Annotated, TypedDict
|
from typing_extensions import Annotated, TypedDict
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class PatternChainState(TypedDict):
|
class PatternChainState(TypedDict):
|
||||||
content_stack: Annotated[Sequence[str], operator.add]
|
content_stack: Annotated[Sequence[str], operator.add]
|
||||||
transformations: List[str]
|
patterns: List[str]
|
||||||
output: str
|
output: str
|
||||||
|
|
||||||
|
|
||||||
def call_model(state: dict, config: RunnableConfig) -> dict:
|
def call_model(state: dict, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", DEFAULT_MODELS.default_transformation_model
|
"model_id", DEFAULT_MODELS.default_transformation_model
|
||||||
)
|
)
|
||||||
transformations = state["transformations"]
|
patterns = state["patterns"]
|
||||||
current_transformation = transformations.pop(0)
|
current_transformation = patterns.pop(0)
|
||||||
if current_transformation.startswith("patterns/"):
|
if current_transformation.startswith("patterns/"):
|
||||||
input_args = {"input_text": state["content_stack"][-1]}
|
input_args = {"input_text": state["content_stack"][-1]}
|
||||||
else:
|
else:
|
||||||
|
|
@ -30,17 +32,17 @@ def call_model(state: dict, config: RunnableConfig) -> dict:
|
||||||
"input_text": state["content_stack"][-1],
|
"input_text": state["content_stack"][-1],
|
||||||
"command": current_transformation,
|
"command": current_transformation,
|
||||||
}
|
}
|
||||||
current_transformation = "patterns/custom"
|
current_transformation = "patterns/default/command"
|
||||||
|
|
||||||
transformation_result = run_pattern(
|
transformation_result = run_pattern(
|
||||||
pattern_name=current_transformation,
|
pattern_name=current_transformation,
|
||||||
model_name=model_name,
|
model_id=model_id,
|
||||||
state=input_args,
|
state=input_args,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"content_stack": [transformation_result.content],
|
"content_stack": [transformation_result.content],
|
||||||
"output": transformation_result.content,
|
"output": transformation_result.content,
|
||||||
"transformations": state["transformations"],
|
"patterns": state["patterns"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ def transform_condition(state: PatternChainState) -> Literal["agent", END]: # t
|
||||||
"""
|
"""
|
||||||
Checks whether there are more chunks to process.
|
Checks whether there are more chunks to process.
|
||||||
"""
|
"""
|
||||||
if len(state["transformations"]) > 0:
|
if len(state["patterns"]) > 0:
|
||||||
return "agent"
|
return "agent"
|
||||||
return END
|
return END
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ from langchain_core.runnables import (
|
||||||
from langgraph.graph import END, START, StateGraph
|
from langgraph.graph import END, START, StateGraph
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class PatternState(TypedDict):
|
class PatternState(TypedDict):
|
||||||
input_text: str
|
input_text: str
|
||||||
|
|
@ -15,13 +17,13 @@ class PatternState(TypedDict):
|
||||||
|
|
||||||
|
|
||||||
def call_model(state: dict, config: RunnableConfig) -> dict:
|
def call_model(state: dict, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", DEFAULT_MODELS.default_transformation_model
|
"model_id", DEFAULT_MODELS.default_transformation_model
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"output": run_pattern(
|
"output": run_pattern(
|
||||||
pattern_name=state["pattern"],
|
pattern_name=state["pattern"],
|
||||||
model_name=model_name,
|
model_id=model_id,
|
||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ from langchain_core.runnables import (
|
||||||
from langgraph.graph import END, START, StateGraph
|
from langgraph.graph import END, START, StateGraph
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
from open_notebook.utils import split_text
|
from open_notebook.utils import split_text
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class TocState(TypedDict):
|
class TocState(TypedDict):
|
||||||
chunks: List[str]
|
chunks: List[str]
|
||||||
|
|
@ -49,13 +51,13 @@ def chunk_condition(state: TocState) -> Literal["get_chunk", END]: # type: igno
|
||||||
|
|
||||||
|
|
||||||
def call_model(state: TocState, config: RunnableConfig) -> dict:
|
def call_model(state: TocState, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", DEFAULT_MODELS.default_transformation_model
|
"model_id", DEFAULT_MODELS.default_transformation_model
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"toc": run_pattern(
|
"toc": run_pattern(
|
||||||
pattern_name="recursive_toc",
|
pattern_name="recursive_toc",
|
||||||
model_name=model_name,
|
model_id=model_id,
|
||||||
state=state,
|
state=state,
|
||||||
).content
|
).content
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ from langgraph.graph import END, START, StateGraph
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.graphs.utils import run_pattern
|
from open_notebook.graphs.utils import run_pattern
|
||||||
from open_notebook.utils import split_text
|
from open_notebook.utils import split_text
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
|
|
||||||
class SummaryResponse(BaseModel):
|
class SummaryResponse(BaseModel):
|
||||||
"""This is schema of your response. Please provide a JSON object with the enclosed keys"""
|
"""This is schema of your response. Please provide a JSON object with the enclosed keys"""
|
||||||
|
|
@ -59,14 +61,14 @@ def chunk_condition(state: SummaryState) -> Literal["get_chunk", END]: # type:
|
||||||
|
|
||||||
|
|
||||||
def call_model(state: dict, config: RunnableConfig) -> dict:
|
def call_model(state: dict, config: RunnableConfig) -> dict:
|
||||||
model_name = config.get("configurable", {}).get(
|
model_id = config.get("configurable", {}).get(
|
||||||
"model_name", DEFAULT_MODELS.default_transformation_model
|
"model_id", DEFAULT_MODELS.default_transformation_model
|
||||||
)
|
)
|
||||||
parser = PydanticOutputParser(pydantic_object=SummaryResponse)
|
parser = PydanticOutputParser(pydantic_object=SummaryResponse)
|
||||||
return {
|
return {
|
||||||
"output": run_pattern(
|
"output": run_pattern(
|
||||||
pattern_name="summarize",
|
pattern_name="summarize",
|
||||||
model_name=model_name,
|
model_id=model_id,
|
||||||
state=state,
|
state=state,
|
||||||
parser=parser,
|
parser=parser,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from langchain.output_parsers import OutputFixingParser
|
from langchain.output_parsers import OutputFixingParser
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from open_notebook.config import DEFAULT_MODELS
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.models import get_model
|
from open_notebook.models import get_model
|
||||||
from open_notebook.prompter import Prompter
|
from open_notebook.prompter import Prompter
|
||||||
from open_notebook.utils import token_count
|
from open_notebook.utils import token_count
|
||||||
|
|
@ -9,36 +9,36 @@ from open_notebook.utils import token_count
|
||||||
|
|
||||||
def run_pattern(
|
def run_pattern(
|
||||||
pattern_name: str,
|
pattern_name: str,
|
||||||
model_name=None,
|
model_id=None,
|
||||||
messages=[],
|
messages=[],
|
||||||
state: dict = {},
|
state: dict = {},
|
||||||
parser=None,
|
parser=None,
|
||||||
output_fixing_model_name=None,
|
output_fixing_model_id=None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
system_prompt = Prompter(prompt_template=pattern_name, parser=parser).render(
|
system_prompt = Prompter(prompt_template=pattern_name, parser=parser).render(
|
||||||
data=state
|
data=state
|
||||||
)
|
)
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
tokens = token_count(str(system_prompt) + str(messages))
|
tokens = token_count(str(system_prompt) + str(messages))
|
||||||
if tokens > 105_000 and DEFAULT_MODELS.large_context_model:
|
|
||||||
model_name = DEFAULT_MODELS.large_context_model
|
|
||||||
logger.debug(
|
|
||||||
f"Using large context model ({model_name}) because the content has {tokens} tokens"
|
|
||||||
)
|
|
||||||
logger.warning(system_prompt)
|
|
||||||
elif tokens > 105_000 and not DEFAULT_MODELS.large_context_model:
|
|
||||||
logger.critical(
|
|
||||||
f"Content has {tokens} tokens, but no large context model is configured"
|
|
||||||
)
|
|
||||||
elif not model_name:
|
|
||||||
model_name = DEFAULT_MODELS.default_transformation_model
|
|
||||||
|
|
||||||
chain = get_model(model_name, model_type="language")
|
if tokens > 105_000:
|
||||||
|
model_id = DEFAULT_MODELS.large_context_model
|
||||||
|
logger.debug(
|
||||||
|
f"Using large context model ({model_id}) because the content has {tokens} tokens"
|
||||||
|
)
|
||||||
|
|
||||||
|
model_id = (
|
||||||
|
model_id
|
||||||
|
or DEFAULT_MODELS.default_transformation_model
|
||||||
|
or DEFAULT_MODELS.default_chat_model
|
||||||
|
)
|
||||||
|
|
||||||
|
chain = get_model(model_id, model_type="language")
|
||||||
if parser:
|
if parser:
|
||||||
chain = chain | parser
|
chain = chain | parser
|
||||||
|
|
||||||
if output_fixing_model_name and parser:
|
if output_fixing_model_id and parser:
|
||||||
output_fix_model = get_model(output_fixing_model_name, model_type="language")
|
output_fix_model = get_model(output_fixing_model_id, model_type="language")
|
||||||
chain = chain | OutputFixingParser.from_llm(
|
chain = chain | OutputFixingParser.from_llm(
|
||||||
parser=parser,
|
parser=parser,
|
||||||
llm=output_fix_model,
|
llm=output_fix_model,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from humanize import naturaltime
|
from humanize import naturaltime
|
||||||
|
|
||||||
|
from open_notebook.config import load_default_models
|
||||||
from open_notebook.domain.notebook import Notebook
|
from open_notebook.domain.notebook import Notebook
|
||||||
from stream_app.chat import chat_sidebar
|
from stream_app.chat import chat_sidebar
|
||||||
from stream_app.note import add_note, note_card
|
from stream_app.note import add_note, note_card
|
||||||
|
|
@ -11,7 +12,6 @@ st.set_page_config(
|
||||||
layout="wide", page_title="📒 Open Notebook", initial_sidebar_state="expanded"
|
layout="wide", page_title="📒 Open Notebook", initial_sidebar_state="expanded"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
version_sidebar()
|
version_sidebar()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,6 +71,9 @@ def notebook_page(current_notebook_id):
|
||||||
sources = current_notebook.sources
|
sources = current_notebook.sources
|
||||||
notes = current_notebook.notes
|
notes = current_notebook.notes
|
||||||
|
|
||||||
|
# Load the default models dynamically
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
notebook_header(current_notebook)
|
notebook_header(current_notebook)
|
||||||
|
|
||||||
work_tab, chat_tab = st.columns([4, 2])
|
work_tab, chat_tab = st.columns([4, 2])
|
||||||
|
|
@ -116,7 +119,6 @@ if st.session_state["current_notebook"]:
|
||||||
st.title("📒 My Notebooks")
|
st.title("📒 My Notebooks")
|
||||||
st.caption("Here are all your notebooks")
|
st.caption("Here are all your notebooks")
|
||||||
|
|
||||||
|
|
||||||
notebooks = Notebook.get_all(order_by="updated desc")
|
notebooks = Notebook.get_all(order_by="updated desc")
|
||||||
|
|
||||||
for notebook in notebooks:
|
for notebook in notebooks:
|
||||||
|
|
|
||||||
|
|
@ -221,10 +221,15 @@ with templates_tab:
|
||||||
key=f"voice2_{pd_config.id}",
|
key=f"voice2_{pd_config.id}",
|
||||||
help="You can use Elevenlabs voice ID",
|
help="You can use Elevenlabs voice ID",
|
||||||
)
|
)
|
||||||
|
if pd_config.model not in provider_models[pd_config.provider]:
|
||||||
|
st.warning(f"Model {pd_config.model} not setup. Changing to default.")
|
||||||
|
index = 0
|
||||||
|
else:
|
||||||
|
index = provider_models[pd_config.provider].index(pd_config.model)
|
||||||
pd_config.model = st.selectbox(
|
pd_config.model = st.selectbox(
|
||||||
"Model",
|
"Model",
|
||||||
provider_models[pd_config.provider],
|
provider_models[pd_config.provider],
|
||||||
index=provider_models[pd_config.provider].index(pd_config.model),
|
index=index,
|
||||||
key=f"model_{pd_config.id}",
|
key=f"model_{pd_config.id}",
|
||||||
)
|
)
|
||||||
st.caption(
|
st.caption(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ st.set_page_config(
|
||||||
version_sidebar()
|
version_sidebar()
|
||||||
|
|
||||||
|
|
||||||
st.title("Settings")
|
st.title("⚙️ Settings")
|
||||||
|
|
||||||
model_tab, model_defaults_tab = st.tabs(["Models", "Model Defaults"])
|
model_tab, model_defaults_tab = st.tabs(["Models", "Model Defaults"])
|
||||||
|
|
||||||
44
pages/8_🛝_Playground.py
Normal file
44
pages/8_🛝_Playground.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import streamlit as st
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from open_notebook.domain.models import Model
|
||||||
|
from open_notebook.graphs.multipattern import graph as pattern_graph
|
||||||
|
from stream_app.utils import version_sidebar
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
layout="wide", page_title="🛝 Playground", initial_sidebar_state="expanded"
|
||||||
|
)
|
||||||
|
version_sidebar()
|
||||||
|
|
||||||
|
st.title("🛝 Playground")
|
||||||
|
with open("transformations.yaml", "r") as file:
|
||||||
|
transformations = yaml.safe_load(file)
|
||||||
|
|
||||||
|
insight_transformations = transformations["source_insights"]
|
||||||
|
|
||||||
|
transformation = st.selectbox(
|
||||||
|
"Pick a transformation",
|
||||||
|
insight_transformations,
|
||||||
|
format_func=lambda x: x.get("name", "No Name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with st.expander("Details"):
|
||||||
|
st.json(transformation)
|
||||||
|
|
||||||
|
models = Model.get_models_by_type("language")
|
||||||
|
model = st.selectbox(
|
||||||
|
"Pick a pattern model",
|
||||||
|
models,
|
||||||
|
format_func=lambda x: x.name,
|
||||||
|
)
|
||||||
|
input_text = st.text_area("Enter some text", height=200)
|
||||||
|
|
||||||
|
if st.button("Run"):
|
||||||
|
output = pattern_graph.invoke(
|
||||||
|
dict(
|
||||||
|
content_stack=[input_text],
|
||||||
|
patterns=transformation["patterns"],
|
||||||
|
),
|
||||||
|
config=dict(configurable={"model_id": model.id}),
|
||||||
|
)
|
||||||
|
st.markdown(output["output"])
|
||||||
1150
poetry.lock
generated
1150
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +0,0 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
|
||||||
|
|
||||||
{{command}}
|
|
||||||
|
|
||||||
# INPUT
|
|
||||||
|
|
||||||
{{input_text}}
|
|
||||||
|
|
||||||
# OUTPUT
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
# IDENTITY and PURPOSE
|
# IDENTITY and PURPOSE
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
Please clean-up the following text, fixing the paragraphs, ponctuation, etc.
|
Please clean-up the following text, fixing the paragraphs, ponctuation, etc.
|
||||||
If you find any word or name mispellings, feel free to correct.
|
If you find any word or name mispellings, feel free to correct.
|
||||||
9
prompts/patterns/default/command.jinja
Normal file
9
prompts/patterns/default/command.jinja
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
|
{{command}}
|
||||||
|
|
||||||
|
# INPUT
|
||||||
|
|
||||||
|
{{input_text}}
|
||||||
|
|
||||||
|
# OUTPUT
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
# IDENTITY and PURPOSE
|
# IDENTITY and PURPOSE
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
# MISSION
|
# MISSION
|
||||||
You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR.
|
You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR.
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
{% include 'patterns/common_text.jinja' %}
|
|
||||||
|
|
||||||
# IDENTITY and PURPOSE
|
# IDENTITY and PURPOSE
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
{% include 'patterns/default/common_tranformation_instructions.jinja' %}
|
||||||
|
|
||||||
# SYSTEM ROLE
|
# SYSTEM ROLE
|
||||||
You are a content summarization assistant that creates dense, information-rich summaries optimized for machine understanding. Your summaries should capture key concepts with minimal words while maintaining complete, clear sentences.
|
You are a content summarization assistant that creates dense, information-rich summaries optimized for machine understanding. Your summaries should capture key concepts with minimal words while maintaining complete, clear sentences.
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{% include 'patterns/common_text.jinja' %}
|
|
||||||
|
|
||||||
Please translate the following text to portuguese:
|
|
||||||
|
|
||||||
|
|
||||||
{{input_text}}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "open-notebook"
|
name = "open-notebook"
|
||||||
version = "0.0.7"
|
version = "0.0.8"
|
||||||
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
|
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
|
||||||
authors = ["Luis Novo <lfnovo@gmail.com>"]
|
authors = ["Luis Novo <lfnovo@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,10 @@ def note_panel(session_id=None, note_id=None):
|
||||||
|
|
||||||
def make_note_from_chat(content, notebook_id=None):
|
def make_note_from_chat(content, notebook_id=None):
|
||||||
# todo: make this more efficient
|
# todo: make this more efficient
|
||||||
transformations = [
|
patterns = [
|
||||||
"Based on the Note below, please provide a Title for this content, with max 15 words"
|
"Based on the Note below, please provide a Title for this content, with max 15 words"
|
||||||
]
|
]
|
||||||
output = pattern_graph.invoke(
|
output = pattern_graph.invoke(dict(content_stack=[content], patterns=patterns))
|
||||||
dict(content_stack=[content], transformations=transformations)
|
|
||||||
)
|
|
||||||
title = surreal_clean(output["output"])
|
title = surreal_clean(output["output"])
|
||||||
|
|
||||||
note = Note(
|
note = Note(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import yaml
|
||||||
from humanize import naturaltime
|
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, load_default_models
|
||||||
from open_notebook.domain.notebook import Asset, Source
|
from open_notebook.domain.notebook import Asset, Source
|
||||||
from open_notebook.exceptions import UnsupportedTypeException
|
from open_notebook.exceptions import UnsupportedTypeException
|
||||||
from open_notebook.graphs.content_processing import graph
|
from open_notebook.graphs.content_processing import graph
|
||||||
|
|
@ -16,11 +16,11 @@ from open_notebook.utils import surreal_clean
|
||||||
|
|
||||||
from .consts import context_icons
|
from .consts import context_icons
|
||||||
|
|
||||||
|
DEFAULT_MODELS, EMBEDDING_MODEL, SPEECH_TO_TEXT_MODEL = load_default_models()
|
||||||
|
|
||||||
def run_transformations(input_text, transformations):
|
|
||||||
output = transform_graph.invoke(
|
def run_patterns(input_text, patterns):
|
||||||
dict(content_stack=[input_text], transformations=transformations)
|
output = transform_graph.invoke(dict(content_stack=[input_text], patterns=patterns))
|
||||||
)
|
|
||||||
return output["output"]
|
return output["output"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,8 +66,8 @@ def source_panel(source_id):
|
||||||
if st.button(
|
if st.button(
|
||||||
transformation["name"], help=transformation["description"]
|
transformation["name"], help=transformation["description"]
|
||||||
):
|
):
|
||||||
result = run_transformations(
|
result = run_patterns(
|
||||||
source.full_text, transformation["transformations"]
|
source.full_text, transformation["patterns"]
|
||||||
)
|
)
|
||||||
source.add_insight(
|
source.add_insight(
|
||||||
transformation["insight_type"], surreal_clean(result)
|
transformation["insight_type"], surreal_clean(result)
|
||||||
|
|
@ -164,7 +164,7 @@ def add_source(session_id):
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(e)
|
st.exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,33 @@ source_insights:
|
||||||
- name: "Summarize"
|
- name: "Summarize"
|
||||||
insight_type: "Content Summary"
|
insight_type: "Content Summary"
|
||||||
description: "Summarize the content"
|
description: "Summarize the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/makeitdense
|
- patterns/default/makeitdense
|
||||||
- patterns/summarize
|
- patterns/default/summarize
|
||||||
- name: "Key Insights"
|
- name: "Key Insights"
|
||||||
insight_type: "Key Insights"
|
insight_type: "Key Insights"
|
||||||
description: "Extracts a list of the Key Insights of the content"
|
description: "Extracts a list of the Key Insights of the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/keyinsights
|
- patterns/default/keyinsights
|
||||||
- name: "Make it Dense"
|
- name: "Make it Dense"
|
||||||
insight_type: "Dense Representation"
|
insight_type: "Dense Representation"
|
||||||
description: "Create a dense representation of the content"
|
description: "Create a dense representation of the content"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/makeitdense
|
- patterns/default/makeitdense
|
||||||
- name: "Analyze Paper"
|
- name: "Analyze Paper"
|
||||||
insight_type: "Paper Analysis"
|
insight_type: "Paper Analysis"
|
||||||
description: "Analyze the paper and provide a quick summary"
|
description: "Analyze the paper and provide a quick summary"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/analyze_paper
|
- patterns/default/analyze_paper
|
||||||
- name: "Reflection"
|
- name: "Reflection"
|
||||||
insight_type: "Reflection Questions"
|
insight_type: "Reflection Questions"
|
||||||
description: "Generates a list of insightful questions to provoke reflection"
|
description: "Generates a list of insightful questions to provoke reflection"
|
||||||
transformations:
|
patterns:
|
||||||
- patterns/reflection_questions
|
- patterns/default/reflection_questions
|
||||||
# - name: "Reflection [PT]"
|
# - name: "Reflection [PT]"
|
||||||
# insight_type: "Reflection Questions [PT]"
|
# insight_type: "Reflection Questions [PT]"
|
||||||
# description: "Generates a list of insightful questions to provoke reflection"
|
# description: "Generates a list of insightful questions to provoke reflection"
|
||||||
# transformations:
|
# patterns:
|
||||||
# - patterns/reflection_questions
|
# - patterns/default/reflection_questions
|
||||||
# - patterns/translate
|
# - patterns/user/translate
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue