-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
216 lines (176 loc) · 6.97 KB
/
app.py
File metadata and controls
216 lines (176 loc) · 6.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""hackNY Public Interest Lab -- Intake Processor
A Streamlit app that demonstrates modular AI-powered entity extraction
and relationship mapping for nonprofit intake processing.
Run with: streamlit run app.py
"""
import json
import os
from pathlib import Path
import streamlit as st
from processor.pipeline import IntakePipeline
from backend import LocalBackend, ClaudeBackend, OpenAIBackend
EXAMPLES_DIR = Path(__file__).parent / "examples"
def load_example(filename: str) -> str:
return (EXAMPLES_DIR / filename).read_text()
def get_backend(choice: str):
"""Instantiate the selected backend."""
if choice == "Local (spaCy)":
return LocalBackend()
elif choice == "Claude (Anthropic)":
api_key = st.session_state.get("anthropic_key") or os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
st.error("Enter your Anthropic API key in the sidebar to use Claude.")
return None
return ClaudeBackend(api_key=api_key)
elif choice == "GPT (OpenAI)":
api_key = st.session_state.get("openai_key") or os.environ.get("OPENAI_API_KEY")
if not api_key:
st.error("Enter your OpenAI API key in the sidebar to use GPT.")
return None
return OpenAIBackend(api_key=api_key)
return None
def render_entities(entities: dict) -> None:
"""Display extracted entities as colored badges."""
colors = {
"people": "#4CAF50",
"orgs": "#2196F3",
"locations": "#FF9800",
"dates": "#9C27B0",
"money": "#F44336",
"events": "#00BCD4",
}
for category, items in entities.items():
if not items:
continue
st.markdown(f"**{category.title()}**")
badges = []
for item in items:
color = colors.get(category, "#757575")
badges.append(
f'<span style="background-color:{color};color:white;'
f'padding:2px 8px;border-radius:12px;margin:2px;'
f'display:inline-block;font-size:0.85em">{item["text"]}</span>'
)
st.markdown(" ".join(badges), unsafe_allow_html=True)
def render_relationships(relationships: list[dict]) -> None:
"""Display relationships as a structured table."""
if not relationships:
st.info("No relationships detected.")
return
for rel in relationships:
conf = rel.get("confidence", 0)
bar = int(conf * 10) * "\u2588" + (10 - int(conf * 10)) * "\u2591"
st.markdown(
f"**{rel['source']}** \u2192 *{rel['relation']}* \u2192 **{rel['target']}** "
f"`{bar}` ({conf:.0%})"
)
def main() -> None:
st.set_page_config(
page_title="PIL Intake Processor",
page_icon="\U0001f50d",
layout="wide",
)
st.title("hackNY Public Interest Lab")
st.subheader("AI-Powered Intake Processor")
st.caption(
"Paste unstructured nonprofit text. Get structured entities, "
"relationships, and follow-up actions."
)
# --- Sidebar ---
with st.sidebar:
st.header("Configuration")
backend_choice = st.selectbox(
"AI Backend",
["Local (spaCy)", "Claude (Anthropic)", "GPT (OpenAI)"],
help="Local runs entirely on your machine with no API keys. "
"Claude and GPT provide enhanced relationship inference.",
)
if backend_choice == "Claude (Anthropic)":
st.text_input(
"Anthropic API Key",
type="password",
key="anthropic_key",
help="Get a key at console.anthropic.com",
)
elif backend_choice == "GPT (OpenAI)":
st.text_input(
"OpenAI API Key",
type="password",
key="openai_key",
help="Get a key at platform.openai.com",
)
st.divider()
st.header("About")
st.markdown(
"This tool demonstrates **modular AI** for nonprofit "
"relationship management. The AI backend is swappable: "
"organizations choose based on data sensitivity, cost, "
"and institutional requirements.\n\n"
"**Stage 1** (always runs): spaCy NER extracts entities locally.\n\n"
"**Stage 2** (optional): An LLM infers relationships and context.\n\n"
"Built by the [hackNY Public Interest Lab](https://hackny.org)."
)
# --- Main area ---
col1, col2 = st.columns([1, 1])
with col1:
st.markdown("### Input")
# Example buttons
example_cols = st.columns(3)
with example_cols[0]:
if st.button("Email Example", use_container_width=True):
st.session_state["input_text"] = load_example("nonprofit_email.txt")
with example_cols[1]:
if st.button("Intake Form", use_container_width=True):
st.session_state["input_text"] = load_example("intake_form.txt")
with example_cols[2]:
if st.button("Meeting Notes", use_container_width=True):
st.session_state["input_text"] = load_example("meeting_notes.txt")
text_input = st.text_area(
"Paste unstructured text here",
value=st.session_state.get("input_text", ""),
height=400,
label_visibility="collapsed",
)
process_btn = st.button(
"Process Intake",
type="primary",
use_container_width=True,
)
with col2:
st.markdown("### Output")
if process_btn and text_input.strip():
backend = get_backend(backend_choice)
if backend is None:
st.stop()
with st.spinner(f"Processing with {backend.name}..."):
pipeline = IntakePipeline(backend)
result = pipeline.process(text_input)
st.session_state["last_result"] = result
result = st.session_state.get("last_result")
if result:
# Summary
st.markdown("#### Summary")
st.info(result["summary"])
# Categories
if result["categories"]:
cats = ", ".join(result["categories"])
st.markdown(f"**Categories:** {cats}")
st.markdown(f"**Backend:** {result['backend_used']} | "
f"**Sentences:** {result['sentence_count']}")
# Entities
st.markdown("#### Extracted Entities")
render_entities(result["entities"])
# Relationships
st.markdown("#### Relationships")
render_relationships(result["relationships"])
# Follow-ups
st.markdown("#### Suggested Follow-ups")
for fu in result["follow_ups"]:
st.markdown(f"- {fu}")
# Raw JSON
with st.expander("Raw JSON Output"):
st.json(result)
elif process_btn:
st.warning("Please enter some text to process.")
if __name__ == "__main__":
main()