Spaces:
Build error
Build error
refactor to modular structure
Browse files- .gitignore +2 -0
- planning_ai/chains/__init__.py +0 -0
- planning_ai/chains/map_chain.py +23 -0
- planning_ai/chains/prompts/map.txt +4 -0
- planning_ai/chains/prompts/reduce.txt +31 -0
- planning_ai/chains/reduce_chain.py +25 -0
- planning_ai/common/utils.py +31 -0
- planning_ai/graph.py +26 -0
- planning_ai/llms/__init__.py +0 -0
- planning_ai/llms/llm.py +11 -0
- planning_ai/main.py +28 -0
- planning_ai/nodes/__init__.py +0 -0
- planning_ai/nodes/map_node.py +38 -0
- planning_ai/nodes/reduce_node.py +24 -0
- planning_ai/preprocessing/gclp.py +16 -0
- src/gpt4o_structured.py → planning_ai/preprocessing/process_pdfs.py +15 -25
- planning_ai/preprocessing/prompts/ocr.txt +10 -0
- planning_ai/preprocessing/web_comments.py +14 -0
- planning_ai/states.py +16 -0
- pyproject.toml +9 -7
- src/planning_ai/__init__.py +0 -2
- src/planning_ai/loaders.py +0 -4
- src/planning_ai/phi.py +0 -91
- uv.lock +0 -0
.gitignore
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
data/
|
|
|
2 |
|
3 |
.envrc
|
4 |
|
@@ -164,3 +165,4 @@ cython_debug/
|
|
164 |
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
165 |
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
166 |
#.idea/
|
|
|
|
1 |
data/
|
2 |
+
.old/
|
3 |
|
4 |
.envrc
|
5 |
|
|
|
165 |
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
166 |
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
167 |
#.idea/
|
168 |
+
.aider*
|
planning_ai/chains/__init__.py
ADDED
File without changes
|
planning_ai/chains/map_chain.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.output_parsers import StrOutputParser
|
2 |
+
from langchain_core.prompts import ChatPromptTemplate
|
3 |
+
|
4 |
+
from planning_ai.llms.llm import LLM
|
5 |
+
|
6 |
+
with open("./planning_ai/chains/prompts/map.txt", "r") as f:
|
7 |
+
map_template = f.read()
|
8 |
+
|
9 |
+
map_prompt = ChatPromptTemplate.from_messages([("system", map_template)])
|
10 |
+
map_chain = map_prompt | LLM | StrOutputParser()
|
11 |
+
|
12 |
+
if __name__ == "__main__":
|
13 |
+
test_document = """
|
14 |
+
The Local Plan proposes a mass development north-west of Cambridge despite marked growth
|
15 |
+
in the last twenty years or so following the previous New Settlement Study. In this period,
|
16 |
+
the major settlement of Cambourne has been created - now over the projected 3,000 homes and
|
17 |
+
Papworth Everard has grown beyond recognition. This in itself is a matter of concern.
|
18 |
+
"""
|
19 |
+
|
20 |
+
result = map_chain.invoke({"context": test_document})
|
21 |
+
|
22 |
+
print("Generated Summary:")
|
23 |
+
print(result)
|
planning_ai/chains/prompts/map.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Summarise the following response to a planning application concisely. Ensure the summary accurately reflects the key points of the response. After the summary, provide one word that represents the author's overall stance: either 'SUPPORT' or 'OPPOSE'.
|
2 |
+
|
3 |
+
Response:
|
4 |
+
{context}
|
planning_ai/chains/prompts/reduce.txt
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The following are summaries of responses to a local government planning application:
|
2 |
+
|
3 |
+
{context}
|
4 |
+
|
5 |
+
Create a final, consolidated summary of the main themes, and follow the specified format precisely. For each key point add inline citations which relate to the sources of the information. Use '[SOURCE_NUMBER]' for the citation (e.g. 'The Space Needle is in Seattle [1][2]').". Each summary will have OPPOSE or SUPPORT appended which indicates which grouping their main argument belongs to. Bare in mind that a summary may contain both supporting and opposing key points.
|
6 |
+
|
7 |
+
Format:
|
8 |
+
|
9 |
+
# Summary
|
10 |
+
|
11 |
+
<Concise summary of the overall themes>
|
12 |
+
|
13 |
+
# Key points raised in support
|
14 |
+
|
15 |
+
Support: <Total number of responses supporting the application>
|
16 |
+
|
17 |
+
* <Key point 1>
|
18 |
+
* <Key point 2>
|
19 |
+
* ...
|
20 |
+
|
21 |
+
# Key points raised in opposition
|
22 |
+
|
23 |
+
Opposed: <Total number of responses opposing the application>
|
24 |
+
|
25 |
+
* <Key point 1>
|
26 |
+
* <Key point 2>
|
27 |
+
* ...
|
28 |
+
|
29 |
+
# Thematic breakdown
|
30 |
+
|
31 |
+
<Provide a breakdown of the key themes identified (e.g. environmental concerns, economic growth), along with the percentage of responses addressing each theme.>
|
planning_ai/chains/reduce_chain.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.output_parsers import StrOutputParser
|
2 |
+
from langchain_core.prompts import ChatPromptTemplate
|
3 |
+
|
4 |
+
from planning_ai.llms.llm import LLM
|
5 |
+
|
6 |
+
with open("./planning_ai/chains/prompts/reduce.txt", "r") as f:
|
7 |
+
reduce_template = f.read()
|
8 |
+
|
9 |
+
reduce_prompt = ChatPromptTemplate([("human", reduce_template)])
|
10 |
+
reduce_chain = reduce_prompt | LLM | StrOutputParser()
|
11 |
+
|
12 |
+
if __name__ == "__main__":
|
13 |
+
test_document = """
|
14 |
+
The response expresses concern over the proposed mass development north-west of Cambridge,
|
15 |
+
highlighting significant growth in the area over the past twenty years, particularly with
|
16 |
+
the establishment of Cambourne and the expansion of Papworth Everard. The author is worried
|
17 |
+
about the implications of further development given the existing growth.
|
18 |
+
|
19 |
+
OPPOSE
|
20 |
+
"""
|
21 |
+
|
22 |
+
result = reduce_chain.invoke({"context": test_document})
|
23 |
+
|
24 |
+
print("Generated Report:")
|
25 |
+
print(result)
|
planning_ai/common/utils.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
from typing import List
|
3 |
+
|
4 |
+
import polars as pl
|
5 |
+
from langchain_core.documents import Document
|
6 |
+
|
7 |
+
from planning_ai.llms.llm import LLM
|
8 |
+
|
9 |
+
pl.Config(
|
10 |
+
fmt_str_lengths=9,
|
11 |
+
set_tbl_rows=5,
|
12 |
+
set_tbl_hide_dtype_separator=True,
|
13 |
+
set_tbl_dataframe_shape_below=True,
|
14 |
+
set_tbl_formatting="UTF8_FULL_CONDENSED",
|
15 |
+
)
|
16 |
+
|
17 |
+
|
18 |
+
class Paths:
|
19 |
+
DATA = Path("data")
|
20 |
+
RAW = DATA / "raw"
|
21 |
+
STAGING = DATA / "staging"
|
22 |
+
OUT = DATA / "out"
|
23 |
+
|
24 |
+
|
25 |
+
class Consts:
|
26 |
+
TOKEN_MAX = 10_000
|
27 |
+
|
28 |
+
|
29 |
+
def length_function(documents: List[Document]) -> int:
|
30 |
+
"""Get number of tokens for input contents."""
|
31 |
+
return sum(LLM.get_num_tokens(doc.page_content) for doc in documents)
|
planning_ai/graph.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langgraph.graph import END, START, StateGraph
|
2 |
+
|
3 |
+
from planning_ai.nodes.map_node import (
|
4 |
+
collect_summaries,
|
5 |
+
generate_summary,
|
6 |
+
map_summaries,
|
7 |
+
should_collapse,
|
8 |
+
)
|
9 |
+
from planning_ai.nodes.reduce_node import collapse_summaries, generate_final_summary
|
10 |
+
from planning_ai.states import OverallState
|
11 |
+
|
12 |
+
|
13 |
+
def create_graph():
|
14 |
+
graph = StateGraph(OverallState)
|
15 |
+
graph.add_node("generate_summary", generate_summary)
|
16 |
+
graph.add_node("collect_summaries", collect_summaries)
|
17 |
+
graph.add_node("collapse_summaries", collapse_summaries)
|
18 |
+
graph.add_node("generate_final_summary", generate_final_summary)
|
19 |
+
|
20 |
+
graph.add_conditional_edges(START, map_summaries, ["generate_summary"])
|
21 |
+
graph.add_edge("generate_summary", "collect_summaries")
|
22 |
+
graph.add_conditional_edges("collect_summaries", should_collapse)
|
23 |
+
graph.add_conditional_edges("collapse_summaries", should_collapse)
|
24 |
+
graph.add_edge("generate_final_summary", END)
|
25 |
+
|
26 |
+
return graph.compile()
|
planning_ai/llms/__init__.py
ADDED
File without changes
|
planning_ai/llms/llm.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
from langchain_core.rate_limiters import InMemoryRateLimiter
|
3 |
+
from langchain_openai import ChatOpenAI
|
4 |
+
|
5 |
+
load_dotenv()
|
6 |
+
|
7 |
+
rate_limiter = InMemoryRateLimiter(
|
8 |
+
requests_per_second=5,
|
9 |
+
check_every_n_seconds=0.1,
|
10 |
+
)
|
11 |
+
LLM = ChatOpenAI(temperature=0, model="gpt-4o-mini", rate_limiter=rate_limiter)
|
planning_ai/main.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_community.document_loaders import DirectoryLoader, TextLoader
|
2 |
+
from langchain_text_splitters import CharacterTextSplitter
|
3 |
+
|
4 |
+
from planning_ai.common.utils import Paths
|
5 |
+
from planning_ai.graph import create_graph
|
6 |
+
|
7 |
+
loader = DirectoryLoader(
|
8 |
+
path=str(Paths.STAGING),
|
9 |
+
show_progress=True,
|
10 |
+
use_multithreading=True,
|
11 |
+
loader_cls=TextLoader,
|
12 |
+
recursive=True,
|
13 |
+
)
|
14 |
+
docs = [doc for doc in loader.load() if doc.page_content]
|
15 |
+
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
|
16 |
+
chunk_size=1000, chunk_overlap=0
|
17 |
+
)
|
18 |
+
split_docs = text_splitter.split_documents(docs)
|
19 |
+
|
20 |
+
app = create_graph()
|
21 |
+
|
22 |
+
for step in app.stream(
|
23 |
+
{"contents": [doc.page_content for doc in split_docs]},
|
24 |
+
{"recursion_limit": 10},
|
25 |
+
):
|
26 |
+
print(list(step.keys()))
|
27 |
+
|
28 |
+
print(step["generate_final_summary"]["final_summary"])
|
planning_ai/nodes/__init__.py
ADDED
File without changes
|
planning_ai/nodes/map_node.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Literal
|
2 |
+
|
3 |
+
from langchain_core.documents import Document
|
4 |
+
from langgraph.constants import Send
|
5 |
+
|
6 |
+
from planning_ai.chains.map_chain import map_chain
|
7 |
+
from planning_ai.common.utils import Consts, length_function
|
8 |
+
from planning_ai.states import OverallState, SummaryState
|
9 |
+
|
10 |
+
|
11 |
+
def generate_summary(state: SummaryState):
|
12 |
+
response = map_chain.invoke({"context": state["content"]})
|
13 |
+
return {"summaries": [response]}
|
14 |
+
|
15 |
+
|
16 |
+
def map_summaries(state: OverallState):
|
17 |
+
return [
|
18 |
+
Send("generate_summary", {"content": content}) for content in state["contents"]
|
19 |
+
]
|
20 |
+
|
21 |
+
|
22 |
+
def collect_summaries(state: OverallState):
|
23 |
+
return {
|
24 |
+
"collapsed_summaries": [
|
25 |
+
Document(f"[{idx}]\n\n{summary}")
|
26 |
+
for idx, summary in enumerate(state["summaries"], start=1)
|
27 |
+
]
|
28 |
+
}
|
29 |
+
|
30 |
+
|
31 |
+
def should_collapse(
|
32 |
+
state: OverallState,
|
33 |
+
) -> Literal["collapse_summaries", "generate_final_summary"]:
|
34 |
+
num_tokens = length_function(state["collapsed_summaries"])
|
35 |
+
if num_tokens > Consts.TOKEN_MAX:
|
36 |
+
return "collapse_summaries"
|
37 |
+
else:
|
38 |
+
return "generate_final_summary"
|
planning_ai/nodes/reduce_node.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain.chains.combine_documents.reduce import collapse_docs, split_list_of_docs
|
2 |
+
|
3 |
+
from planning_ai.chains.reduce_chain import reduce_chain
|
4 |
+
from planning_ai.common.utils import Consts, length_function
|
5 |
+
from planning_ai.states import OverallState
|
6 |
+
|
7 |
+
|
8 |
+
def collapse_summaries(state: OverallState):
|
9 |
+
doc_lists = split_list_of_docs(
|
10 |
+
state["collapsed_summaries"], length_function, Consts.TOKEN_MAX
|
11 |
+
)
|
12 |
+
results = []
|
13 |
+
for doc_list in doc_lists:
|
14 |
+
results.append(collapse_docs(doc_list, reduce_chain.invoke))
|
15 |
+
|
16 |
+
return {"collapsed_summaries": results}
|
17 |
+
|
18 |
+
|
19 |
+
def generate_final_summary(state: OverallState):
|
20 |
+
response = reduce_chain.invoke({"context": state["collapsed_summaries"]})
|
21 |
+
return {
|
22 |
+
"final_summary": response,
|
23 |
+
"collapsed_summaries": state["collapsed_summaries"],
|
24 |
+
}
|
planning_ai/preprocessing/gclp.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import polars as pl
|
2 |
+
|
3 |
+
from planning_ai.common.utils import Paths
|
4 |
+
|
5 |
+
df = pl.read_excel(
|
6 |
+
Paths.RAW / "gclp-first-proposals-questionnaire-responses-redacted.xlsx"
|
7 |
+
)
|
8 |
+
|
9 |
+
free_cols = [df.columns[0]] + df.columns[6:13] + [df.columns[33]]
|
10 |
+
df = df[free_cols]
|
11 |
+
|
12 |
+
for row in df.rows(named=True):
|
13 |
+
user = row.pop("UserNo")
|
14 |
+
content = "\n\n".join([f"**{k}**\n\n{v}" for k, v in row.items() if v != "-"])
|
15 |
+
with open(Paths.STAGING / "gclp" / f"{user}.txt", "w") as f:
|
16 |
+
f.write(content)
|
src/gpt4o_structured.py → planning_ai/preprocessing/process_pdfs.py
RENAMED
@@ -1,19 +1,16 @@
|
|
1 |
-
import ast
|
2 |
import base64
|
3 |
import os
|
4 |
from io import BytesIO
|
5 |
from pathlib import Path
|
6 |
|
7 |
-
import polars as pl
|
8 |
import requests
|
9 |
from dotenv import load_dotenv
|
10 |
from pdf2image import convert_from_path
|
11 |
|
12 |
-
|
13 |
-
|
|
|
14 |
|
15 |
-
def convert_pdf_to_images(file_path):
|
16 |
-
return convert_from_path(file_path)
|
17 |
|
18 |
def encode_images_to_base64(images):
|
19 |
image_b64 = []
|
@@ -29,6 +26,7 @@ def encode_images_to_base64(images):
|
|
29 |
)
|
30 |
return image_b64
|
31 |
|
|
|
32 |
def send_request_to_api(messages):
|
33 |
api_key = os.getenv("OPENAI_API_KEY")
|
34 |
headers = {
|
@@ -41,37 +39,29 @@ def send_request_to_api(messages):
|
|
41 |
)
|
42 |
return response.json()
|
43 |
|
44 |
-
def main():
|
45 |
-
load_environment()
|
46 |
-
|
47 |
-
prompt = """
|
48 |
-
The following images are from a planning response form completed by a member of the public. They contain free-form responses related to a planning application, which may be either handwritten or typed.
|
49 |
|
50 |
-
|
51 |
-
"""
|
|
|
|
|
52 |
|
53 |
-
|
54 |
-
for file in path.glob("*.pdf"):
|
55 |
if file.stem:
|
56 |
-
images =
|
57 |
image_b64 = encode_images_to_base64(images)
|
58 |
|
59 |
messages = [
|
60 |
{
|
61 |
"role": "user",
|
62 |
-
"content": [
|
63 |
-
{
|
64 |
-
"type": "text",
|
65 |
-
"text": prompt,
|
66 |
-
},
|
67 |
-
]
|
68 |
-
+ image_b64,
|
69 |
}
|
70 |
]
|
71 |
|
72 |
response = send_request_to_api(messages)
|
73 |
-
|
74 |
-
|
|
|
|
|
75 |
|
76 |
if __name__ == "__main__":
|
77 |
main()
|
|
|
|
|
1 |
import base64
|
2 |
import os
|
3 |
from io import BytesIO
|
4 |
from pathlib import Path
|
5 |
|
|
|
6 |
import requests
|
7 |
from dotenv import load_dotenv
|
8 |
from pdf2image import convert_from_path
|
9 |
|
10 |
+
from planning_ai.common.utils import Paths
|
11 |
+
|
12 |
+
load_dotenv()
|
13 |
|
|
|
|
|
14 |
|
15 |
def encode_images_to_base64(images):
|
16 |
image_b64 = []
|
|
|
26 |
)
|
27 |
return image_b64
|
28 |
|
29 |
+
|
30 |
def send_request_to_api(messages):
|
31 |
api_key = os.getenv("OPENAI_API_KEY")
|
32 |
headers = {
|
|
|
39 |
)
|
40 |
return response.json()
|
41 |
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
+
def main():
|
44 |
+
pdfs = (Paths.RAW / "pdfs").glob("*.pdf")
|
45 |
+
with open("planning_ai/preprocessing/prompts/ocr.txt", "r") as f:
|
46 |
+
ocr_prompt = f.read()
|
47 |
|
48 |
+
for file in pdfs:
|
|
|
49 |
if file.stem:
|
50 |
+
images = convert_from_path(file)
|
51 |
image_b64 = encode_images_to_base64(images)
|
52 |
|
53 |
messages = [
|
54 |
{
|
55 |
"role": "user",
|
56 |
+
"content": [{"type": "text", "text": ocr_prompt}] + image_b64,
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
}
|
58 |
]
|
59 |
|
60 |
response = send_request_to_api(messages)
|
61 |
+
out = response["choices"][0]["message"]["content"]
|
62 |
+
with open(Paths.STAGING / "pdfs" / f"{file.stem}.txt", "w") as f:
|
63 |
+
f.write(out)
|
64 |
+
|
65 |
|
66 |
if __name__ == "__main__":
|
67 |
main()
|
planning_ai/preprocessing/prompts/ocr.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The images provided are from a planning response form filled out by a member of the public, containing free-form responses related to a planning application. These responses may be handwritten or typed.
|
2 |
+
|
3 |
+
Please follow these instructions to process the images:
|
4 |
+
|
5 |
+
1. **Extract Free-Form Information Only**: Focus on extracting and outputting the free-form written content from the images. Do not include single-word answers, brief responses, or any extra content that is not part of the detailed responses.
|
6 |
+
2. **Verbatim Output**: Ensure that the extracted information is output exactly as it appears in the images. Add a heading before each section of free-form text if it helps with organisation, but ensure the heading is not added by the model itself. Ignore blank sections entirely—do not generate or include any additional thoughts or content.
|
7 |
+
3. **Sequential Processing**: The images are sequentially ordered. A response might continue from one image to the next, so capture the full context across multiple images if necessary.
|
8 |
+
4. **Ignore Non-Relevant Content**: Exclude any content that does not fit the criteria of free-form, detailed responses.
|
9 |
+
|
10 |
+
Thank you for your attention to these details.
|
planning_ai/preprocessing/web_comments.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import polars as pl
|
2 |
+
|
3 |
+
from planning_ai.common.utils import Paths
|
4 |
+
|
5 |
+
dfs = pl.read_excel(Paths.RAW / "web comments.xlsx", sheet_id=0)
|
6 |
+
|
7 |
+
for sheet_name, df in dfs.items():
|
8 |
+
string_df = df.select(pl.col(pl.String)).drop_nulls()
|
9 |
+
for col in string_df.columns:
|
10 |
+
series = string_df[col]
|
11 |
+
name = series.name
|
12 |
+
content = f"**{name}**" + "\n\n* ".join(["\n"] + series.to_list())
|
13 |
+
with open(Paths.STAGING / "web" / f"{sheet_name}.txt", "w") as f:
|
14 |
+
f.write(content)
|
planning_ai/states.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import operator
|
3 |
+
from typing import Annotated, List, TypedDict
|
4 |
+
|
5 |
+
from langchain_core.documents import Document
|
6 |
+
|
7 |
+
class OverallState(TypedDict):
|
8 |
+
contents: List[str]
|
9 |
+
summaries: Annotated[list, operator.add]
|
10 |
+
collapsed_summaries: List[Document]
|
11 |
+
final_summary: str
|
12 |
+
|
13 |
+
|
14 |
+
class SummaryState(TypedDict):
|
15 |
+
content: str
|
16 |
+
|
pyproject.toml
CHANGED
@@ -5,16 +5,18 @@ description = "Add your description here"
|
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.10,<3.11"
|
7 |
dependencies = [
|
|
|
8 |
"langchain-core>=0.2.38",
|
9 |
"langchain-community>=0.2.16",
|
10 |
-
"langchain-
|
11 |
-
"transformers>=4.44.2",
|
12 |
-
"torch>=2.4.1",
|
13 |
-
"accelerate>=0.34.0",
|
14 |
"pillow>=10.4.0",
|
15 |
-
"
|
16 |
-
"
|
17 |
-
"
|
|
|
|
|
|
|
|
|
18 |
"pdf2image>=1.17.0",
|
19 |
]
|
20 |
|
|
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.10,<3.11"
|
7 |
dependencies = [
|
8 |
+
"python-dotenv>=1.0.1",
|
9 |
"langchain-core>=0.2.38",
|
10 |
"langchain-community>=0.2.16",
|
11 |
+
"langchain-openai>=0.1.23",
|
|
|
|
|
|
|
12 |
"pillow>=10.4.0",
|
13 |
+
"polars>=1.6.0",
|
14 |
+
"fastexcel>=0.11.6",
|
15 |
+
"spacy>=3.7.6",
|
16 |
+
"pip>=24.2",
|
17 |
+
"spacytextblob>=4.0.0",
|
18 |
+
"transformers>=4.44.2",
|
19 |
+
"langgraph>=0.2.18",
|
20 |
"pdf2image>=1.17.0",
|
21 |
]
|
22 |
|
src/planning_ai/__init__.py
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
def hello() -> str:
|
2 |
-
return "Hello from planning-ai!"
|
|
|
|
|
|
src/planning_ai/loaders.py
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
from langchain_unstructured import UnstructuredLoader
|
2 |
-
|
3 |
-
loader = UnstructuredLoader("./data/raw/pdfs/57693-94 Response Form.pdf")
|
4 |
-
loader.load()
|
|
|
|
|
|
|
|
|
|
src/planning_ai/phi.py
DELETED
@@ -1,91 +0,0 @@
|
|
1 |
-
from pathlib import Path
|
2 |
-
|
3 |
-
from pdf2image import convert_from_path
|
4 |
-
from PIL import Image
|
5 |
-
from transformers import AutoModelForCausalLM, AutoProcessor
|
6 |
-
|
7 |
-
model_id = "microsoft/Phi-3.5-vision-instruct"
|
8 |
-
|
9 |
-
# Note: set _attn_implementation='eager' if you don't have flash_attn installed
|
10 |
-
model = AutoModelForCausalLM.from_pretrained(
|
11 |
-
model_id,
|
12 |
-
device_map="cuda",
|
13 |
-
trust_remote_code=True,
|
14 |
-
torch_dtype="auto",
|
15 |
-
_attn_implementation="flash_attention_2",
|
16 |
-
)
|
17 |
-
|
18 |
-
# for best performance, use num_crops=4 for multi-frame, num_crops=16 for single-frame.
|
19 |
-
processor = AutoProcessor.from_pretrained(
|
20 |
-
model_id, trust_remote_code=True, num_crops=16
|
21 |
-
)
|
22 |
-
|
23 |
-
images = []
|
24 |
-
placeholder = ""
|
25 |
-
path = Path("./data/raw/pdfs")
|
26 |
-
i = 1
|
27 |
-
for file in path.glob("*.pdf"):
|
28 |
-
pdf_images = convert_from_path(file)
|
29 |
-
for image in pdf_images:
|
30 |
-
images.append(image)
|
31 |
-
placeholder += f"<|image_{i}|>\n"
|
32 |
-
i += 1
|
33 |
-
|
34 |
-
messages = [
|
35 |
-
{
|
36 |
-
"role": "user",
|
37 |
-
"content": """
|
38 |
-
<|image_1|>
|
39 |
-
|
40 |
-
This image is an extract from a planning response form filled out by a member of the public. The form may contain typed or handwritten responses, including potentially incomplete or unclear sections. Your task is to extract relevant information in a strict, structured format. Do not repeat the document verbatim. Only output responses in the structured format below.
|
41 |
-
|
42 |
-
Instructions:
|
43 |
-
1. Extract responses to all structured questions on the form, in the format:
|
44 |
-
{"<question>": "<response>"}
|
45 |
-
|
46 |
-
Example:
|
47 |
-
{"Do you support the planning proposal?": "Yes"}
|
48 |
-
|
49 |
-
2. For the handwritten notes under 'Your comments:', extract them verbatim. If any word is illegible or unclear, use the token <UNKNOWN>. Do not attempt to infer or complete missing parts. Use the format:
|
50 |
-
{"Your comments:": "<verbatim comments>"}
|
51 |
-
|
52 |
-
Example:
|
53 |
-
{"Your comments:": "I support the proposal, but the <UNKNOWN> aspect requires attention."}
|
54 |
-
|
55 |
-
3. **Do not** output or repeat the original document content in full. Only return structured data in the format described above.
|
56 |
-
4. **Ignore irrelevant sections** that are not part of the structured questionnaire or 'Your comments:' section.
|
57 |
-
5. If a response is missing or the form section is blank, output:
|
58 |
-
{"<question>": "No response"}
|
59 |
-
|
60 |
-
Guidelines:
|
61 |
-
- Ensure you return only structured data in JSON-like format.
|
62 |
-
- Strictly follow the format for both structured questions and handwritten comments.
|
63 |
-
- If any part of the form is unclear or unreadable, do not fill it in with assumptions.
|
64 |
-
- Avoid repeating the full content of the form. Focus only on extracting the relevant sections.
|
65 |
-
|
66 |
-
Example output:
|
67 |
-
{
|
68 |
-
"Do you support the planning proposal?": "Yes",
|
69 |
-
"Your comments:": "The proposal seems reasonable, but <UNKNOWN> needs further assessment."
|
70 |
-
}
|
71 |
-
""",
|
72 |
-
}
|
73 |
-
]
|
74 |
-
|
75 |
-
prompt = processor.tokenizer.apply_chat_template(
|
76 |
-
messages, tokenize=False, add_generation_prompt=True
|
77 |
-
)
|
78 |
-
|
79 |
-
inputs = processor(prompt, images[1], return_tensors="pt").to("cuda:0")
|
80 |
-
generation_args = {"max_new_tokens": 10_000}
|
81 |
-
|
82 |
-
generate_ids = model.generate(
|
83 |
-
**inputs, eos_token_id=processor.tokenizer.eos_token_id, **generation_args
|
84 |
-
)
|
85 |
-
|
86 |
-
# remove input tokens
|
87 |
-
generate_ids = generate_ids[:, inputs["input_ids"].shape[1] :]
|
88 |
-
response = processor.batch_decode(
|
89 |
-
generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
90 |
-
)[0]
|
91 |
-
print(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uv.lock
CHANGED
The diff for this file is too large to render.
See raw diff
|
|