📁 File Structure
projects/myproplex/
├── app.py— CLI terminal version of the research assistant
├── web_app.py— Streamlit web interface (main app)
├── system-prompt.txt— Agent persona, legal focus, and 3-sentence format rules
├── requirements.txt— Python dependencies
├── .env.example— API key template
├── demo.html— Static interactive demo page
├── user-guide.md— Plain language setup and usage guide
├── problem-statement.md— Problem definition and target users
└── project-outline.md— App name, features, tech stack, roadmap
🔑 Key Code: Agent Loop (web_app.py)
The core of the app is a ReAct-style agent loop. Claude decides whether to search the web, calls the Tavily tool, reads results, and iterates until ready to answer. This loop runs inside Streamlit's chat interface.
def run_agent(user_message, history, client, tavily, system_prompt):
history.append({"role": "user", "content": user_message})
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=system_prompt,
tools=TOOLS,
messages=history
)
if response.stop_reason == "end_turn":
text = "\n".join(b.text for b in response.content if hasattr(b, "text"))
history.append({"role": "assistant", "content": response.content})
return text
elif response.stop_reason == "tool_use":
history.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use" and block.name == "search_web":
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": search_web(block.input["query"], tavily)
})
history.append({"role": "user", "content": tool_results})
The history list is stored in st.session_state.conv_history and persists across turns, giving the agent memory within a session.
🔑 Key Code: API Secret Handling
The app reads API keys from Streamlit secrets when deployed on Streamlit Cloud, and falls back to a local .env file when running locally. This means the same code works in both environments with no changes.
@st.cache_resource
def init_clients():
try:
ak = st.secrets["ANTHROPIC_API_KEY"]
tk = st.secrets["TAVILY_API_KEY"]
except (KeyError, FileNotFoundError):
ak = os.getenv("ANTHROPIC_API_KEY")
tk = os.getenv("TAVILY_API_KEY")
if not ak or not tk:
return None, None
return Anthropic(api_key=ak), TavilyClient(api_key=tk)
🔑 Key Code: System Prompt (system-prompt.txt)
The system prompt is what turns a general AI into a specialist legal research tool. The 3-sentence format rule is the most impactful instruction — it forces Claude to be concise and structured on every answer.
- Maximum 3 sentences per answer. No exceptions.
- Sentence 1: The direct answer in plain, everyday language.
- Sentence 2: The legal basis — act name and section number, stated simply.
- Sentence 3: One important caveat, exception, or next step if needed.
- Never use bullet points, headers, or lists. Write in plain sentences only.
Short, clear, and simple. Write as if explaining to a smart person
who is not a lawyer. No jargon. No long sentences.
📦 Dependencies (requirements.txt)
anthropic>=0.40.0
tavily-python>=0.3.0
python-dotenv>=1.0.0
streamlit>=1.32.0