⚖️ MyPropLex

Source Code — Malaysian Property Law Research Assistant

📁 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): # Add user message to conversation history history.append({"role": "user", "content": user_message}) while True: # Call Claude with tools available 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": # Claude is done — extract and return the text answer 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": # Claude wants to search — execute the tool and feed results back 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 Streamlit Cloud secrets first (production) try: ak = st.secrets["ANTHROPIC_API_KEY"] tk = st.secrets["TAVILY_API_KEY"] except (KeyError, FileNotFoundError): # Fall back to .env file (local development) 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.

FORMAT OF YOUR ANSWERS - 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. TONE 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 # Claude API SDK tavily-python>=0.3.0 # Web search API python-dotenv>=1.0.0 # Load .env files locally streamlit>=1.32.0 # Web interface framework