Last updated

Building a Legal Practice Memory System

This tutorial demonstrates how to build a memory system for a legal practice that processes contracts, extracts entities, tracks client matters, and enables quick access to relevant precedents and contract terms.

What We'll Build

A legal practice memory system that:

  1. Defines a custom legal schema for contracts, parties, and obligations
  2. Uploads and processes legal documents with automatic entity extraction
  3. Stores lawyer notes and insights about cases
  4. Queries contract information and precedents with natural language
  5. Analyzes portfolio with GraphQL for insights

Use Case

Sarah is a corporate lawyer managing multiple client contracts. She needs to:

  • Quickly find similar contract clauses across her portfolio
  • Track obligations and deadlines for all active contracts
  • Remember key negotiation points and client preferences
  • Analyze contract terms and values across her practice

Prerequisites

  • Papr Memory API key (Get one here)
  • Python 3.8+ or Node.js 16+
  • Sample legal documents (PDFs or Word docs)

First, create a schema that defines the entities and relationships in legal contracts.

from papr_memory import Papr
import os

client = Papr(x_api_key=os.environ.get("PAPR_MEMORY_API_KEY"))

# Define legal contract schema
legal_schema = client.schemas.create(
    name="Legal Contract Schema",
    description="Schema for managing legal contracts, parties, obligations, and case notes",
    version="1.0.0",
    node_types={
        "Contract": {
            "name": "Contract",
            "label": "Contract",
            "description": "Legal contract or agreement document",
            "properties": {
                "title": {
                    "type": "string",
                    "required": True,
                    "description": "Full contract title as it appears in the document"
                },
                "contract_type": {
                    "type": "string",
                    "required": True,
                    "description": "Type of legal contract",
                    "enum_values": ["service", "employment", "nda", "partnership", "license"]
                },
                "value": {
                    "type": "float",
                    "required": False,
                    "description": "Total contract value in USD"
                },
                "effective_date": {
                    "type": "datetime",
                    "required": False,
                    "description": "Date when contract becomes effective"
                },
                "expiration_date": {
                    "type": "datetime",
                    "required": False,
                    "description": "Date when contract expires"
                },
                "status": {
                    "type": "string",
                    "required": False,
                    "description": "Current status of the contract",
                    "enum_values": ["draft", "negotiating", "active", "expired", "terminated"],
                    "default": "draft"
                },
                "client_id": {
                    "type": "string",
                    "required": False,
                    "description": "Client identifier for this contract"
                }
            },
            "required_properties": ["title", "contract_type"],
            "unique_identifiers": ["title"],
            "color": "#e74c3c"
        },
        "Party": {
            "name": "Party",
            "label": "Party",
            "description": "Legal party to a contract",
            "properties": {
                "name": {
                    "type": "string",
                    "required": True,
                    "description": "Full legal name of the party"
                },
                "party_type": {
                    "type": "string",
                    "required": False,
                    "description": "Type of party",
                    "enum_values": ["individual", "company", "government", "nonprofit"]
                },
                "role": {
                    "type": "string",
                    "required": False,
                    "description": "Role in the contract",
                    "enum_values": ["client", "counterparty", "vendor", "partner"]
                }
            },
            "required_properties": ["name"],
            "unique_identifiers": ["name"],
            "color": "#3498db"
        },
        "Obligation": {
            "name": "Obligation",
            "label": "Obligation",
            "description": "Contractual obligation or deadline",
            "properties": {
                "description": {
                    "type": "string",
                    "required": True,
                    "description": "Description of the obligation"
                },
                "deadline": {
                    "type": "datetime",
                    "required": False,
                    "description": "Deadline for fulfilling the obligation"
                },
                "status": {
                    "type": "string",
                    "required": False,
                    "description": "Current status",
                    "enum_values": ["pending", "completed", "overdue"],
                    "default": "pending"
                },
                "responsible_party": {
                    "type": "string",
                    "required": False,
                    "description": "Party responsible for this obligation"
                }
            },
            "required_properties": ["description"],
            "unique_identifiers": []
        },
        "CaseNote": {
            "name": "CaseNote",
            "label": "Case Note",
            "description": "Lawyer's notes about a case or contract",
            "properties": {
                "note": {
                    "type": "string",
                    "required": True,
                    "description": "Note content"
                },
                "note_type": {
                    "type": "string",
                    "required": False,
                    "description": "Type of note",
                    "enum_values": ["negotiation", "client_preference", "risk", "precedent"]
                },
                "created_date": {
                    "type": "datetime",
                    "required": False,
                    "description": "Date note was created"
                }
            },
            "required_properties": ["note"],
            "unique_identifiers": []
        }
    },
    relationship_types={
        "PARTY_TO": {
            "name": "PARTY_TO",
            "label": "Party To",
            "description": "Party is signatory to the contract",
            "allowed_source_types": ["Party"],
            "allowed_target_types": ["Contract"]
        },
        "HAS_OBLIGATION": {
            "name": "HAS_OBLIGATION",
            "label": "Has Obligation",
            "description": "Contract includes an obligation",
            "allowed_source_types": ["Contract"],
            "allowed_target_types": ["Obligation"]
        },
        "NOTE_ABOUT": {
            "name": "NOTE_ABOUT",
            "label": "Note About",
            "description": "Note is about a contract",
            "allowed_source_types": ["CaseNote"],
            "allowed_target_types": ["Contract"]
        }
    }
)

schema_id = legal_schema.data.id
print(f"Legal schema created: {schema_id}")

Step 2: Upload Contract Documents

Sarah uploads a service agreement for her client Acme Corp.

# Upload a service agreement
doc_response = client.document.upload(
    file=open("acme_service_agreement.pdf", "rb"),
    schema_id=schema_id,
    simple_schema_mode=True,
    hierarchical_enabled=True,
    user_id="lawyer_sarah",  # Associate with Sarah's workspace
    property_overrides=[
        {
            "nodeLabel": "Contract",
            "set": {
                "status": "negotiating",
                "client_id": "acme_corp"
            }
        },
        {
            "nodeLabel": "Party",
            "match": {"name": "Acme Corp"},
            "set": {
                "id": "party_acme",
                "role": "client"
            }
        }
    ]
)

upload_id = doc_response.document_status.upload_id
print(f"Document uploaded: {upload_id}")

# Monitor processing
import time
while True:
    status = client.document.get_status(upload_id)
    print(f"Progress: {status.progress * 100}%")
    
    if status.status_type == "completed":
        print("✓ Contract processed successfully!")
        break
    elif status.status_type == "failed":
        print(f"✗ Processing failed: {status.error}")
        break
    
    time.sleep(5)

Step 3: Add Lawyer Notes and Insights

Sarah adds her notes about the negotiation and client preferences.

# Add negotiation note
negotiation_note = client.memory.add(
    content="Acme Corp negotiation: Client strongly prefers termination for convenience clause. Previous contracts show they value flexibility over cost savings. Suggested 30-day notice period was well received.",
    user_id="lawyer_sarah",
    metadata={
        "client_id": "acme_corp",
        "contract_type": "service",
        "topics": ["negotiation", "termination_clause", "client_preference"]
    },
    graph_generation={
        "mode": "auto",
        "auto": {
            "schema_id": schema_id,
            "simple_schema_mode": True
        }
    }
)

# Add risk assessment note
risk_note = client.memory.add(
    content="Risk assessment for Acme service agreement: Payment terms of Net-60 are longer than our standard Net-30. Recommend adding late payment penalties of 1.5% monthly interest. Client has good payment history based on previous contracts.",
    user_id="lawyer_sarah",
    metadata={
        "client_id": "acme_corp",
        "contract_type": "service",
        "topics": ["risk", "payment_terms", "client_history"]
    },
    graph_generation={
        "mode": "auto",
        "auto": {
            "schema_id": schema_id,
            "simple_schema_mode": True
        }
    }
)

# Add precedent note
precedent_note = client.memory.add(
    content="Similar termination clause language used successfully in TechCo agreement (2023). Client accepted 30-day notice with mutual termination rights. Language can be reused as precedent.",
    user_id="lawyer_sarah",
    metadata={
        "topics": ["precedent", "termination_clause", "contract_template"]
    },
    graph_generation={
        "mode": "auto",
        "auto": {
            "schema_id": schema_id,
            "simple_schema_mode": True
        }
    }
)

print("✓ Notes added to memory system")

Step 4: Query with Natural Language

Sarah can now search her practice memory for relevant information.

# Find contracts expiring soon
expiring_response = client.memory.search(
    query="What contracts are expiring in the next 90 days?",
    user_id="lawyer_sarah",
    enable_agentic_graph=True,
    max_memories=20,
    max_nodes=15
)

print(f"Found {len(expiring_response.data.memories)} contracts expiring soon")
for node in expiring_response.data.nodes:
    if node.label == "Contract":
        print(f"\n{node.properties.get('title')}")
        print(f"  Expires: {node.properties.get('expiration_date')}")
        print(f"  Client: {node.properties.get('client_id')}")

# Find precedents for termination clauses
precedent_response = client.memory.search(
    query="Show me precedents for termination clauses with 30-day notice",
    user_id="lawyer_sarah",
    enable_agentic_graph=True,
    max_memories=10
)

print(f"\n\nFound {len(precedent_response.data.memories)} relevant precedents")
for memory in precedent_response.data.memories:
    print(f"\n{memory.content[:200]}...")

# Find client preferences
preference_response = client.memory.search(
    query="What are Acme Corp's contract preferences and negotiation history?",
    user_id="lawyer_sarah",
    metadata_filter={"client_id": "acme_corp"},
    enable_agentic_graph=True,
    max_memories=10
)

print(f"\n\nFound {len(preference_response.data.memories)} client preferences")
for memory in preference_response.data.memories:
    print(f"- {memory.content[:150]}...")

# Find obligations due this month
obligations_response = client.memory.search(
    query="What contract obligations are due this month?",
    user_id="lawyer_sarah",
    enable_agentic_graph=True,
    max_memories=20,
    max_nodes=20
)

print(f"\n\nFound {len(obligations_response.data.nodes)} obligations")
for node in obligations_response.data.nodes:
    if node.label == "Obligation":
        print(f"- {node.properties.get('description')}")
        print(f"  Deadline: {node.properties.get('deadline')}")
        print(f"  Status: {node.properties.get('status')}")

Step 5: Analyze Portfolio with GraphQL

Sarah uses GraphQL to analyze her entire contract portfolio.

# Get portfolio summary
portfolio_summary = client.graphql.query(
    query="""
    query PortfolioSummary($userId: String!) {
      contracts(where: { user_id: $userId }) {
        total: aggregate { count }
        by_type {
          contract_type
          count: aggregate { count }
          total_value: aggregate { sum(field: "value") }
        }
        by_status {
          status
          count: aggregate { count }
        }
        by_client {
          client_id
          count: aggregate { count }
          total_value: aggregate { sum(field: "value") }
        }
      }
    }
    """,
    variables={"userId": "lawyer_sarah"}
)

print("\n=== Portfolio Summary ===")
summary = portfolio_summary.data['contracts']
print(f"Total contracts: {summary['total']['count']}")

print("\nBy Type:")
for type_data in summary['by_type']:
    print(f"  {type_data['contract_type']}: {type_data['count']} contracts")
    print(f"    Total value: ${type_data['total_value']:,.2f}")

print("\nBy Status:")
for status_data in summary['by_status']:
    print(f"  {status_data['status']}: {status_data['count']} contracts")

# Find upcoming deadlines
upcoming_obligations = client.graphql.query(
    query="""
    query UpcomingDeadlines($userId: String!, $startDate: DateTime!, $endDate: DateTime!) {
      obligations(
        where: {
          contract: { user_id: $userId }
          deadline: { _gte: $startDate, _lte: $endDate }
          status: "pending"
        }
        order_by: { deadline: asc }
      ) {
        description
        deadline
        responsible_party
        contract {
          title
          client_id
        }
      }
    }
    """,
    variables={
        "userId": "lawyer_sarah",
        "startDate": "2024-01-01T00:00:00Z",
        "endDate": "2024-03-31T23:59:59Z"
    }
)

print("\n=== Upcoming Obligations (Next 90 Days) ===")
for obligation in upcoming_obligations.data['obligations']:
    print(f"\n{obligation['description']}")
    print(f"  Deadline: {obligation['deadline']}")
    print(f"  Contract: {obligation['contract']['title']}")
    print(f"  Client: {obligation['contract']['client_id']}")

# Analyze client relationships
client_analysis = client.graphql.query(
    query="""
    query ClientAnalysis($userId: String!) {
      parties(
        where: { 
          role: "client"
          contracts: { user_id: $userId }
        }
      ) {
        name
        contract_count: contracts_aggregate { count }
        total_value: contracts_aggregate { 
          sum { value } 
        }
        active_contracts: contracts(
          where: { status: "active" }
        ) {
          title
          contract_type
          expiration_date
        }
        notes: case_notes {
          note
          note_type
        }
      }
    }
    """,
    variables={"userId": "lawyer_sarah"}
)

print("\n=== Client Relationship Analysis ===")
for client in client_analysis.data['parties']:
    print(f"\n{client['name']}")
    print(f"  Total contracts: {client['contract_count']}")
    print(f"  Total value: ${client['total_value']['sum']:,.2f}")
    print(f"  Active contracts: {len(client['active_contracts'])}")
    if client['notes']:
        print(f"  Key notes:")
        for note in client['notes'][:3]:
            print(f"    - [{note['note_type']}] {note['note'][:100]}...")

Step 6: Build Practice Dashboard

Create a comprehensive view of the practice.

class LegalPracticeMemory:
    def __init__(self, api_key: str, lawyer_id: str, schema_id: str):
        self.client = Papr(x_api_key=api_key)
        self.lawyer_id = lawyer_id
        self.schema_id = schema_id
    
    def upload_contract(self, file_path: str, client_id: str, contract_type: str):
        """Upload and process a contract"""
        with open(file_path, "rb") as f:
            response = self.client.document.upload(
                file=f,
                schema_id=self.schema_id,
                simple_schema_mode=True,
                hierarchical_enabled=True,
                user_id=self.lawyer_id,
                property_overrides=[
                    {
                        "nodeLabel": "Contract",
                        "set": {"client_id": client_id, "contract_type": contract_type}
                    }
                ]
            )
        return response.document_status.upload_id
    
    def add_case_note(self, content: str, client_id: str, note_type: str, topics: list):
        """Add a case note"""
        return self.client.memory.add(
            content=content,
            user_id=self.lawyer_id,
            metadata={
                "client_id": client_id,
                "note_type": note_type,
                "topics": topics
            },
            graph_generation={
                "mode": "auto",
                "auto": {
                    "schema_id": self.schema_id,
                    "simple_schema_mode": True
                }
            }
        )
    
    def find_precedents(self, query: str):
        """Find relevant precedents"""
        response = self.client.memory.search(
            query=query,
            user_id=self.lawyer_id,
            metadata_filter={"note_type": "precedent"},
            enable_agentic_graph=True,
            max_memories=10
        )
        return response.data.memories
    
    def get_client_history(self, client_id: str):
        """Get complete client history"""
        response = self.client.memory.search(
            query=f"Complete history and notes for {client_id}",
            user_id=self.lawyer_id,
            metadata_filter={"client_id": client_id},
            enable_agentic_graph=True,
            max_memories=50,
            max_nodes=30
        )
        return {
            "memories": response.data.memories,
            "entities": response.data.nodes
        }
    
    def get_expiring_contracts(self, days: int = 90):
        """Get contracts expiring within specified days"""
        response = self.client.graphql.query(
            query="""
            query ExpiringContracts($userId: String!, $cutoffDate: DateTime!) {
              contracts(
                where: {
                  user_id: $userId
                  expiration_date: { _lte: $cutoffDate }
                  status: "active"
                }
                order_by: { expiration_date: asc }
              ) {
                title
                client_id
                expiration_date
                value
                parties {
                  name
                  role
                }
              }
            }
            """,
            variables={
                "userId": self.lawyer_id,
                "cutoffDate": f"2024-{(datetime.now() + timedelta(days=days)).strftime('%m-%d')}T00:00:00Z"
            }
        )
        return response.data['contracts']

# Usage
practice = LegalPracticeMemory(
    api_key=os.environ.get("PAPR_MEMORY_API_KEY"),
    lawyer_id="lawyer_sarah",
    schema_id=schema_id
)

# Upload new contract
upload_id = practice.upload_contract(
    "new_client_agreement.pdf",
    client_id="newcorp",
    contract_type="service"
)

# Add negotiation notes
practice.add_case_note(
    content="NewCorp prefers aggressive SLAs and is willing to pay premium for 99.9% uptime guarantee",
    client_id="newcorp",
    note_type="negotiation",
    topics=["sla", "pricing", "client_preference"]
)

# Find similar precedents
precedents = practice.find_precedents("SLA clauses with 99.9% uptime guarantee")
print(f"Found {len(precedents)} similar precedents")

# Check expiring contracts
expiring = practice.get_expiring_contracts(days=60)
print(f"\n{len(expiring)} contracts expiring in next 60 days")
for contract in expiring:
    print(f"  - {contract['title']} (Client: {contract['client_id']})")
    print(f"    Expires: {contract['expiration_date']}")

Key Benefits

For the Lawyer

  • Quick precedent access: Find similar clauses and language from past contracts
  • Client context: Remember client preferences and negotiation history
  • Deadline tracking: Never miss contract obligations or renewal dates
  • Portfolio insights: Understand practice composition and client relationships

For the Practice

  • Institutional knowledge: Capture and share legal expertise across the team
  • Efficiency: Reuse successful contract language and strategies
  • Risk management: Track obligations and compliance requirements
  • Client service: Provide personalized advice based on complete history

Next Steps