diff --git a/services/graphiti_client.py b/services/graphiti_client.py index 4c93b1f..884d8e3 100644 --- a/services/graphiti_client.py +++ b/services/graphiti_client.py @@ -12,6 +12,8 @@ from graphiti_core.llm_client import LLMConfig from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient from graphiti_core.embedder import OpenAIEmbedder, OpenAIEmbedderConfig +from services.graphiti_tracer import build_langfuse_tracer + class GraphitiError(Exception): """Fehler beim Zugriff auf den Graphiti-Client.""" @@ -89,12 +91,15 @@ async def _build_graphiti() -> Graphiti: ) ) + tracer = build_langfuse_tracer(span_prefix="graphiti", ctx=None) + client = Graphiti( uri=neo4j_uri, user=neo4j_user, password=neo4j_password, llm_client=llm_client, embedder=embedder, + tracer=tracer, ) await client.build_indices_and_constraints() return client diff --git a/services/graphiti_schema.py b/services/graphiti_schema.py new file mode 100644 index 0000000..0a5fbcf --- /dev/null +++ b/services/graphiti_schema.py @@ -0,0 +1,1096 @@ +"""Graphiti Knowledge Graph – Zivilrechtliches Kanzlei-Schema (Graphiti-first). + +Definiert Pydantic-Modelle für Entity- und Edge-Typen, die Graphiti beim +Extrahieren aus zivilrechtlichen Kanzleidokumenten verwenden soll. + +Ziel des Schemas: +- praxisnah für deutsche Zivilrechtskanzleien +- Graphiti-first: wenige, klare, textnah extrahierbare Typen +- saubere Trennung zwischen: + * Tatsachenbehauptungen + * Rechtsbehauptungen / Rechtspositionen + * Auslegungen / Interpretationen + * Beweismitteln + * rechtlichen Autoritäten + * Interessen / Emotionen / internen Bewertungen + +Verwendung in add_episode(): + from services.graphiti_schema import ( + ENTITY_TYPES, + EDGE_TYPES, + EDGE_TYPE_MAP, + EXTRACTION_INSTRUCTIONS, + ) + + await graphiti.add_episode( + ..., + entity_types=ENTITY_TYPES, + edge_types=EDGE_TYPES, + edge_type_map=EDGE_TYPE_MAP, + custom_extraction_instructions=EXTRACTION_INSTRUCTIONS, + ) +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Entity-Typen +# --------------------------------------------------------------------------- + + +class Actor(BaseModel): + """A person or organization relevant to a legal matter, such as a party, + lawyer, witness, court, insurer, authority, expert, or company.""" + + actor_kind: Optional[str] = Field( + default=None, + description=( + "Kind of actor: person, organization, court, authority, insurer, " + "expert, lawyer, judge, notary, unknown" + ), + ) + legal_form: Optional[str] = Field( + default=None, + description="Legal form if applicable, e.g. GmbH, AG, e.V., GbR", + ) + function_title: Optional[str] = Field( + default=None, + description=( + "Official or practical function, e.g. Geschäftsführer, Richterin, " + "Sachverständiger, Rechtsanwalt" + ), + ) + domicile: Optional[str] = Field( + default=None, + description="Residence, seat, registered office, or principal place of business", + ) + + +class Matter(BaseModel): + """A legal matter, mandate, dispute, proceeding, or case container.""" + + matter_type: Optional[str] = Field( + default=None, + description=( + "Type of matter: civil_litigation, advisory, pre_litigation, " + "appeal, enforcement, settlement, contract_review, other" + ), + ) + file_reference: Optional[str] = Field( + default=None, + description="Internal law-firm matter reference or internal file number", + ) + court_reference: Optional[str] = Field( + default=None, + description="Court case number or external file number if available", + ) + instance_level: Optional[str] = Field( + default=None, + description=( + "Procedural level: pre_litigation, first_instance, appeal, revision, " + "enforcement, post_judgment, non_contentious" + ), + ) + status: Optional[str] = Field( + default=None, + description="Current status of the matter", + ) + subject_area: Optional[str] = Field( + default=None, + description=( + "Area of law, e.g. contract, tort, lease, employment, corporate, " + "commercial, inheritance" + ), + ) + + +class Document(BaseModel): + """A document or communication artifact in or around the legal matter.""" + + document_type: Optional[str] = Field( + default=None, + description=( + "Type of document: pleading, brief, statement_of_claim, defense_brief, " + "judgment, order, protocol, contract, addendum, email, letter, invoice, " + "expert_report, note, attachment, chat, settlement_draft, other" + ), + ) + document_date: Optional[datetime] = Field( + default=None, + description="Date the document was created, signed, or issued", + ) + source_kind: Optional[str] = Field( + default=None, + description=( + "Source category: client, opponent, court, internal, third_party, " + "authority, insurer, expert, unknown" + ), + ) + version_label: Optional[str] = Field( + default=None, + description="Version label such as draft, final, executed, amended, signed", + ) + authenticity_status: Optional[str] = Field( + default=None, + description="Authenticity status: original, copy, disputed, verified, unknown", + ) + + +class Event(BaseModel): + """A real-world or procedural event relevant to the matter.""" + + event_type: Optional[str] = Field( + default=None, + description=( + "Type of event: contract_signing, delivery, payment, damage, defect_notice, " + "termination, negotiation, hearing, filing, service, judgment, settlement_talk, other" + ), + ) + event_layer: Optional[str] = Field( + default=None, + description="Layer of event: real_world or procedural", + ) + event_date: Optional[datetime] = Field( + default=None, + description="Date or start date of the event", + ) + end_date: Optional[datetime] = Field( + default=None, + description="End date if the event spans a period", + ) + location: Optional[str] = Field( + default=None, + description="Relevant location of the event if stated", + ) + certainty_level: Optional[str] = Field( + default=None, + description="Occurrence certainty: certain, likely, disputed, unclear", + ) + + +class Issue(BaseModel): + """A factual, legal, interpretive, procedural, damages-related, or strategic issue.""" + + issue_type: Optional[str] = Field( + default=None, + description=( + "Type of issue: factual, legal, interpretive, procedural, evidentiary, " + "damages, limitation, settlement, strategic" + ), + ) + issue_status: Optional[str] = Field( + default=None, + description="Status of the issue: open, disputed, clarified, partially_resolved, decided", + ) + importance: Optional[str] = Field( + default=None, + description="Importance level: low, medium, high, critical", + ) + description: Optional[str] = Field( + default=None, + description="Short issue description", + ) + + +class FactAssertion(BaseModel): + """A factual assertion about what happened, existed, was paid, signed, delivered, + said, known, caused, or otherwise factually occurred.""" + + assertion_text: Optional[str] = Field( + default=None, + description="Atomic factual assertion in natural language", + ) + fact_type: Optional[str] = Field( + default=None, + description=( + "Type of factual assertion: event, state, causation, payment, communication, " + "authorship, authenticity, chronology, amount, identity, knowledge, possession, other" + ), + ) + status: Optional[str] = Field( + default=None, + description=( + "Status in the matter: asserted, disputed, undisputed, admitted, contradicted, " + "established, unclear" + ), + ) + assertion_date: Optional[datetime] = Field( + default=None, + description="Date on which the assertion was made or first recorded", + ) + certainty_level: Optional[str] = Field( + default=None, + description="Confidence in the factual content: certain, likely, uncertain, disputed", + ) + + +class LegalAssertion(BaseModel): + """A legal assertion, legal position, or legal conclusion about rights, + obligations, validity, liability, admissibility, limitation, or legal effect.""" + + assertion_text: Optional[str] = Field( + default=None, + description="Atomic legal assertion in natural language", + ) + legal_type: Optional[str] = Field( + default=None, + description=( + "Type of legal assertion: validity, invalidity, liability, no_liability, " + "admissibility, inadmissibility, limitation, enforceability, jurisdiction, " + "burden, interpretation_result, other" + ), + ) + status: Optional[str] = Field( + default=None, + description="Status of the legal position: asserted, contested, accepted, court_indicated, decided, unclear", + ) + certainty_level: Optional[str] = Field( + default=None, + description="Strength or confidence: strong, moderate, weak, uncertain", + ) + + +class Interpretation(BaseModel): + """An interpretation of a contract, clause, statute, statement, conduct, + communication, or procedural act.""" + + interpretation_text: Optional[str] = Field( + default=None, + description="Natural language statement of the interpretation", + ) + interpretation_type: Optional[str] = Field( + default=None, + description=( + "Type of interpretation: contract, clause, statute, statement, conduct, " + "procedural_act, communication" + ), + ) + method: Optional[str] = Field( + default=None, + description=( + "Interpretive method: wording, systematics, purpose, context, " + "party_intent, case_law_based, mixed" + ), + ) + status: Optional[str] = Field( + default=None, + description="Status: proposed, contested, plausible, preferred, court_adopted, rejected", + ) + certainty_level: Optional[str] = Field( + default=None, + description="Confidence of the interpretation: strong, moderate, weak, uncertain", + ) + + +class Evidence(BaseModel): + """Evidence offered, available, produced, or evaluated in support of factual content.""" + + evidence_type: Optional[str] = Field( + default=None, + description=( + "Type of evidence: document, witness, expert_report, photo, video, audio, " + "log, invoice, chat, email, inspection, party_hearing, other" + ), + ) + evidence_date: Optional[datetime] = Field( + default=None, + description="Date of the evidence item or date it was generated", + ) + origin: Optional[str] = Field( + default=None, + description="Origin of evidence: client, opponent, court, third_party, expert, authority, unknown", + ) + authenticity_status: Optional[str] = Field( + default=None, + description="Authenticity or integrity status: verified, disputed, unknown, partial", + ) + evidentiary_weight: Optional[str] = Field( + default=None, + description="Indicative evidentiary weight: weak, medium, strong, unclear", + ) + evaluation_status: Optional[str] = Field( + default=None, + description="Evaluation state: offered, produced, admitted, examined, evaluated, rejected, not_used", + ) + + +class Authority(BaseModel): + """A legal authority such as a statute, court decision, contract clause, + contract text, or other normative/legal source used in reasoning.""" + + authority_type: Optional[str] = Field( + default=None, + description="Type of authority: statute, case_law, contract_clause, contract, regulation, procedural_rule, other", + ) + citation: Optional[str] = Field( + default=None, + description=( + "Citation or identifier, e.g. § 280 Abs. 1 BGB, § 307 BGB, " + "BGH NJW 2021, 1234, clause 5.2" + ), + ) + authority_date: Optional[datetime] = Field( + default=None, + description="Date of the decision, version, or relevant authority text if available", + ) + court_or_source: Optional[str] = Field( + default=None, + description="Issuing court or source if applicable", + ) + version_label: Optional[str] = Field( + default=None, + description="Version or temporal qualifier if relevant", + ) + + +class Claim(BaseModel): + """A claim, defense, objection, set-off, counterclaim, or procedural application.""" + + claim_type: Optional[str] = Field( + default=None, + description=( + "Type: payment, damages, injunction, declaratory_relief, rescission, " + "setoff, limitation_defense, objection, counterclaim, procedural_application, other" + ), + ) + legal_basis: Optional[str] = Field( + default=None, + description="Stated legal basis, e.g. § 280 BGB, § 823 BGB, contract clause", + ) + amount: Optional[float] = Field( + default=None, + description="Monetary amount if applicable", + ) + currency: Optional[str] = Field( + default=None, + description="Currency code, e.g. EUR", + ) + status: Optional[str] = Field( + default=None, + description="Status: asserted, disputed, pending, partially_granted, granted, rejected, settled, unclear", + ) + limitation_status: Optional[str] = Field( + default=None, + description="Limitation status if relevant: not_raised, raised, disputed, expired, not_expired, unclear", + ) + + +class FinancialItem(BaseModel): + """A financial item such as payment, invoice, receivable, damage item, + fee, interest, or cost item.""" + + item_type: Optional[str] = Field( + default=None, + description="Type: payment, invoice, receivable, damage_item, fee, cost, interest, settlement_amount, other", + ) + amount: Optional[float] = Field( + default=None, + description="Amount of the financial item", + ) + currency: Optional[str] = Field( + default=None, + description="Currency code, e.g. EUR", + ) + booking_date: Optional[datetime] = Field( + default=None, + description="Booking, invoice, payment, or valuation date", + ) + due_date: Optional[datetime] = Field( + default=None, + description="Due date if applicable", + ) + status: Optional[str] = Field( + default=None, + description="Status: claimed, paid, overdue, disputed, offset, partially_paid, written_off, unclear", + ) + + +class Interest(BaseModel): + """A substantive, economic, personal, reputational, relational, or strategic interest.""" + + interest_type: Optional[str] = Field( + default=None, + description="Type of interest: economic, personal, reputational, strategic, relational, procedural", + ) + description: Optional[str] = Field( + default=None, + description="Description of the interest pursued or protected", + ) + priority: Optional[str] = Field( + default=None, + description="Priority level: low, medium, high, critical", + ) + explicitness: Optional[str] = Field( + default=None, + description="How explicit the interest is: explicit, inferred, assumed", + ) + time_relevance: Optional[str] = Field( + default=None, + description="Temporal relevance: immediate, ongoing, long_term", + ) + + +class Emotion(BaseModel): + """An emotional state or emotional stance relevant to conflict dynamics, + credibility, escalation, negotiation, or settlement.""" + + emotion_type: Optional[str] = Field( + default=None, + description="Type: anger, fear, distrust, urgency, willingness, hostility, frustration, relief, uncertainty, other", + ) + intensity: Optional[str] = Field( + default=None, + description="Intensity: low, medium, high", + ) + polarity: Optional[str] = Field( + default=None, + description="Polarity: positive, negative, mixed, neutral", + ) + inferred_from: Optional[str] = Field( + default=None, + description="Short explanation of what the emotional inference is based on", + ) + temporal_scope: Optional[str] = Field( + default=None, + description="Temporal scope: momentary, ongoing, recurring, unclear", + ) + + +class Assessment(BaseModel): + """An internal assessment of risk, opportunity, strategy, weakness, strength, + evidentiary posture, legal strength, or settlement option.""" + + assessment_type: Optional[str] = Field( + default=None, + description=( + "Type: risk, opportunity, strategy, weakness, strength, settlement_option, " + "evidentiary_assessment, legal_assessment, procedural_assessment" + ), + ) + assessment_text: Optional[str] = Field( + default=None, + description="Text of the internal assessment", + ) + severity: Optional[str] = Field( + default=None, + description="Severity or importance: low, medium, high, critical", + ) + likelihood: Optional[str] = Field( + default=None, + description="Likelihood if relevant: low, medium, high, unclear", + ) + assessment_date: Optional[datetime] = Field( + default=None, + description="Date of the assessment", + ) + + +class Deadline(BaseModel): + """A legal, procedural, contractual, or internal deadline or due date.""" + + deadline_type: Optional[str] = Field( + default=None, + description=( + "Type: court_deadline, limitation, reply_deadline, hearing_date, " + "payment_due_date, internal_deadline, appeal_deadline, enforcement_deadline" + ), + ) + due_date: Optional[datetime] = Field( + default=None, + description="Due date or scheduled time", + ) + status: Optional[str] = Field( + default=None, + description="Status: upcoming, met, missed, extended, unclear", + ) + consequence: Optional[str] = Field( + default=None, + description="Potential or actual consequence of meeting or missing the deadline", + ) + + +class InformationGap(BaseModel): + """A missing document, fact, evidence item, clarification, timeline element, + authority, or calculation needed for the matter.""" + + gap_type: Optional[str] = Field( + default=None, + description=( + "Type: missing_document, missing_fact, missing_evidence, missing_calculation, " + "missing_authority, missing_instruction, missing_timeline, other" + ), + ) + description: Optional[str] = Field( + default=None, + description="Description of what is missing", + ) + relevance_level: Optional[str] = Field( + default=None, + description="Relevance: low, medium, high, critical", + ) + requested_from: Optional[str] = Field( + default=None, + description="Who is expected to provide or clarify the missing information", + ) + resolution_status: Optional[str] = Field( + default=None, + description="Status: open, requested, partially_resolved, resolved, unobtainable", + ) + + +# --------------------------------------------------------------------------- +# Edge-Typen +# --------------------------------------------------------------------------- + + +class HasRole(BaseModel): + """Role of an actor in a matter.""" + + role_type: Optional[str] = Field( + default=None, + description=( + "Role in the matter: client, plaintiff, defendant, witness, expert, judge, " + "lawyer, opposing_lawyer, insurer, third_party, debtor, creditor, other" + ), + ) + side: Optional[str] = Field( + default=None, + description="Side in the matter: own_side, opposing_side, neutral, court_side, unclear", + ) + start_date: Optional[datetime] = Field( + default=None, + description="Date the role began", + ) + end_date: Optional[datetime] = Field( + default=None, + description="Date the role ended", + ) + + +class OccursInMatter(BaseModel): + """Connects an entity to the legal matter it belongs to.""" + + relevance: Optional[str] = Field( + default=None, + description="Relevance to the matter: peripheral, relevant, central", + ) + + +class InvolvesActor(BaseModel): + """Indicates involvement of an actor in another entity.""" + + involvement_type: Optional[str] = Field( + default=None, + description=( + "Type of involvement: author, sender, recipient, signatory, participant, " + "subject, holder, affected_party, beneficiary, target, issuer, addressee, other" + ), + ) + + +class ContainsFactAssertion(BaseModel): + """A document contains a factual assertion.""" + + section_reference: Optional[str] = Field( + default=None, + description="Section, page, paragraph, sentence, or exhibit reference if available", + ) + + +class ContainsLegalAssertion(BaseModel): + """A document contains a legal assertion.""" + + section_reference: Optional[str] = Field( + default=None, + description="Section, page, paragraph, sentence, or exhibit reference if available", + ) + + +class ContainsInterpretation(BaseModel): + """A document contains an interpretation.""" + + section_reference: Optional[str] = Field( + default=None, + description="Section, page, paragraph, sentence, or exhibit reference if available", + ) + + +class AssertsFact(BaseModel): + """An actor asserts, denies, admits, reports, or disputes a factual assertion.""" + + modality: Optional[str] = Field( + default=None, + description="Modality: asserts, denies, admits, reports, suspects, recalls, disputes", + ) + assertion_date: Optional[datetime] = Field( + default=None, + description="Date of the assertion if known", + ) + + +class AssertsLegalPosition(BaseModel): + """An actor advances, contests, accepts, or rejects a legal assertion.""" + + modality: Optional[str] = Field( + default=None, + description="Modality: asserts, contests, accepts, proposes, adopts, rejects", + ) + assertion_date: Optional[datetime] = Field( + default=None, + description="Date the legal position was asserted", + ) + + +class ProposesInterpretation(BaseModel): + """An actor proposes, contests, adopts, prefers, or rejects an interpretation.""" + + modality: Optional[str] = Field( + default=None, + description="Modality: proposes, contests, adopts, rejects, prefers", + ) + assertion_date: Optional[datetime] = Field( + default=None, + description="Date the interpretation was proposed or discussed", + ) + + +class SupportsFact(BaseModel): + """Evidence supports or tends to support a factual assertion.""" + + strength: Optional[str] = Field( + default=None, + description="Support strength: weak, medium, strong", + ) + directness: Optional[str] = Field( + default=None, + description="Directness: direct, indirect, circumstantial", + ) + + +class SupportsLegalPosition(BaseModel): + """A legal authority supports or limits a legal assertion.""" + + support_type: Optional[str] = Field( + default=None, + description="Support type: direct, analogous, persuasive, conflicting, limiting", + ) + strength: Optional[str] = Field( + default=None, + description="Support strength: weak, medium, strong", + ) + + +class SupportsInterpretation(BaseModel): + """An authority or factual context supports an interpretation.""" + + support_type: Optional[str] = Field( + default=None, + description="Support type: wording, context, purpose, case_law, factual_context, mixed", + ) + strength: Optional[str] = Field( + default=None, + description="Support strength: weak, medium, strong", + ) + + +class ConcernsIssue(BaseModel): + """Connects an entity to an issue it concerns.""" + + relevance: Optional[str] = Field( + default=None, + description="Relevance to the issue: low, medium, high, critical", + ) + + +class RelatesToClaim(BaseModel): + """Connects an entity to a claim, defense, objection, or application.""" + + relation_type: Optional[str] = Field( + default=None, + description=( + "Relation type: supports, weakens, required_for, contests, quantifies, " + "limits, defeats, substantiates" + ), + ) + + +class Contradicts(BaseModel): + """A contradiction or inconsistency between two elements.""" + + conflict_kind: Optional[str] = Field( + default=None, + description=( + "Kind of conflict: factual, legal, interpretive, evidentiary, chronology, " + "amount, authenticity" + ), + ) + severity: Optional[str] = Field( + default=None, + description="Severity: low, medium, high, critical", + ) + explanation: Optional[str] = Field( + default=None, + description="Short explanation of the contradiction", + ) + + +class Interprets(BaseModel): + """An interpretation interprets a legal authority, document, clause, or legal proposition.""" + + target_aspect: Optional[str] = Field( + default=None, + description="Aspect interpreted: wording, clause_scope, intent, legal_effect, procedural_meaning, other", + ) + + +class TriggersDeadline(BaseModel): + """A document or event triggers a deadline.""" + + trigger_type: Optional[str] = Field( + default=None, + description="Type of trigger: service, filing, judgment, notice, contract_event, invoice, limitation_start, other", + ) + + +class ConcernsFinancialItem(BaseModel): + """Connects a document, event, or claim to a financial item.""" + + relation_type: Optional[str] = Field( + default=None, + description="Relation type: creates, evidences, claims, pays, reduces, offsets, calculates, disputes", + ) + + +class HoldsInterest(BaseModel): + """An actor holds or pursues an interest.""" + + basis: Optional[str] = Field( + default=None, + description="Basis: explicit_statement, negotiation_behavior, correspondence, internal_note, inferred, assumed", + ) + + +class ExperiencesEmotion(BaseModel): + """An actor experiences or displays an emotion or emotional stance.""" + + basis: Optional[str] = Field( + default=None, + description="Basis: explicit_statement, observed_behavior, tone, negotiation, internal_note, inferred", + ) + + +class Influences(BaseModel): + """A strategic, emotional, or analytical factor influences another element.""" + + influence_type: Optional[str] = Field( + default=None, + description=( + "Influence type: strengthens, weakens, delays, escalates, enables_settlement, " + "blocks_settlement, changes_priority, increases_risk" + ), + ) + strength: Optional[str] = Field( + default=None, + description="Influence strength: low, medium, high", + ) + + +class IdentifiesGap(BaseModel): + """An issue, claim, or assessment identifies a missing piece of information.""" + + urgency: Optional[str] = Field( + default=None, + description="Urgency: low, medium, high, critical", + ) + + +class AddressesGap(BaseModel): + """An entity helps resolve an information gap.""" + + resolution_quality: Optional[str] = Field( + default=None, + description="Quality of resolution: partial, substantial, complete", + ) + + +class Assesses(BaseModel): + """An internal assessment evaluates another entity.""" + + assessment_aspect: Optional[str] = Field( + default=None, + description=( + "Aspect evaluated: credibility, legal_strength, evidentiary_strength, " + "settlement_value, procedural_risk, economic_impact, strategic_value" + ), + ) + + +# --------------------------------------------------------------------------- +# Zusammengefasste Exports +# --------------------------------------------------------------------------- + +ENTITY_TYPES: dict[str, type[BaseModel]] = { + "Actor": Actor, + "Matter": Matter, + "Document": Document, + "Event": Event, + "Issue": Issue, + "FactAssertion": FactAssertion, + "LegalAssertion": LegalAssertion, + "Interpretation": Interpretation, + "Evidence": Evidence, + "Authority": Authority, + "Claim": Claim, + "FinancialItem": FinancialItem, + "Interest": Interest, + "Emotion": Emotion, + "Assessment": Assessment, + "Deadline": Deadline, + "InformationGap": InformationGap, +} + +EDGE_TYPES: dict[str, type[BaseModel]] = { + "HasRole": HasRole, + "OccursInMatter": OccursInMatter, + "InvolvesActor": InvolvesActor, + "ContainsFactAssertion": ContainsFactAssertion, + "ContainsLegalAssertion": ContainsLegalAssertion, + "ContainsInterpretation": ContainsInterpretation, + "AssertsFact": AssertsFact, + "AssertsLegalPosition": AssertsLegalPosition, + "ProposesInterpretation": ProposesInterpretation, + "SupportsFact": SupportsFact, + "SupportsLegalPosition": SupportsLegalPosition, + "SupportsInterpretation": SupportsInterpretation, + "ConcernsIssue": ConcernsIssue, + "RelatesToClaim": RelatesToClaim, + "Contradicts": Contradicts, + "Interprets": Interprets, + "TriggersDeadline": TriggersDeadline, + "ConcernsFinancialItem": ConcernsFinancialItem, + "HoldsInterest": HoldsInterest, + "ExperiencesEmotion": ExperiencesEmotion, + "Influences": Influences, + "IdentifiesGap": IdentifiesGap, + "AddressesGap": AddressesGap, + "Assesses": Assesses, +} + +# Erlaubte Kanten pro Entity-Paar: (Quell-Typ, Ziel-Typ) -> [erlaubte Edge-Typen] +EDGE_TYPE_MAP: dict[tuple[str, str], list[str]] = { + ("Actor", "Matter"): ["HasRole"], + ("Document", "Matter"): ["OccursInMatter"], + ("Event", "Matter"): ["OccursInMatter"], + ("Issue", "Matter"): ["OccursInMatter"], + ("Claim", "Matter"): ["OccursInMatter"], + ("Deadline", "Matter"): ["OccursInMatter"], + ("Assessment", "Matter"): ["OccursInMatter"], + ("Interest", "Matter"): ["OccursInMatter"], + ("Emotion", "Matter"): ["OccursInMatter"], + ("InformationGap", "Matter"): ["OccursInMatter"], + ("FinancialItem", "Matter"): ["OccursInMatter"], + ("Authority", "Matter"): ["OccursInMatter"], + ("Evidence", "Matter"): ["OccursInMatter"], + ("FactAssertion", "Matter"): ["OccursInMatter"], + ("LegalAssertion", "Matter"): ["OccursInMatter"], + ("Interpretation", "Matter"): ["OccursInMatter"], + + ("Document", "Actor"): ["InvolvesActor"], + ("Event", "Actor"): ["InvolvesActor"], + ("Claim", "Actor"): ["InvolvesActor"], + ("Interest", "Actor"): ["InvolvesActor"], + ("Emotion", "Actor"): ["InvolvesActor"], + ("FinancialItem", "Actor"): ["InvolvesActor"], + + ("Document", "FactAssertion"): ["ContainsFactAssertion"], + ("Document", "LegalAssertion"): ["ContainsLegalAssertion"], + ("Document", "Interpretation"): ["ContainsInterpretation"], + + ("Actor", "FactAssertion"): ["AssertsFact"], + ("Actor", "LegalAssertion"): ["AssertsLegalPosition"], + ("Actor", "Interpretation"): ["ProposesInterpretation"], + + ("Evidence", "FactAssertion"): ["SupportsFact"], + ("Authority", "LegalAssertion"): ["SupportsLegalPosition"], + ("Authority", "Interpretation"): ["SupportsInterpretation"], + ("FactAssertion", "Interpretation"): ["SupportsInterpretation"], + + ("FactAssertion", "Issue"): ["ConcernsIssue"], + ("LegalAssertion", "Issue"): ["ConcernsIssue"], + ("Interpretation", "Issue"): ["ConcernsIssue"], + ("Evidence", "Issue"): ["ConcernsIssue"], + ("Authority", "Issue"): ["ConcernsIssue"], + ("Claim", "Issue"): ["ConcernsIssue"], + ("FinancialItem", "Issue"): ["ConcernsIssue"], + ("Assessment", "Issue"): ["ConcernsIssue"], + ("Interest", "Issue"): ["ConcernsIssue"], + ("Emotion", "Issue"): ["ConcernsIssue"], + ("Deadline", "Issue"): ["ConcernsIssue"], + ("InformationGap", "Issue"): ["ConcernsIssue"], + + ("FactAssertion", "Claim"): ["RelatesToClaim"], + ("LegalAssertion", "Claim"): ["RelatesToClaim"], + ("Interpretation", "Claim"): ["RelatesToClaim"], + ("Evidence", "Claim"): ["RelatesToClaim"], + ("Authority", "Claim"): ["RelatesToClaim"], + ("FinancialItem", "Claim"): ["RelatesToClaim"], + ("Assessment", "Claim"): ["RelatesToClaim"], + ("Interest", "Claim"): ["RelatesToClaim"], + + ("FactAssertion", "FactAssertion"): ["Contradicts"], + ("LegalAssertion", "LegalAssertion"): ["Contradicts"], + ("Interpretation", "Interpretation"): ["Contradicts"], + ("Evidence", "FactAssertion"): ["Contradicts"], + ("FactAssertion", "Interpretation"): ["Contradicts"], + ("LegalAssertion", "Interpretation"): ["Contradicts"], + + ("Interpretation", "Authority"): ["Interprets"], + ("Interpretation", "Document"): ["Interprets"], + ("Interpretation", "LegalAssertion"): ["Interprets"], + + ("Document", "Deadline"): ["TriggersDeadline"], + ("Event", "Deadline"): ["TriggersDeadline"], + + ("Document", "FinancialItem"): ["ConcernsFinancialItem"], + ("Event", "FinancialItem"): ["ConcernsFinancialItem"], + ("Claim", "FinancialItem"): ["ConcernsFinancialItem"], + + ("Actor", "Interest"): ["HoldsInterest"], + ("Actor", "Emotion"): ["ExperiencesEmotion"], + + ("Interest", "Claim"): ["Influences"], + ("Interest", "Issue"): ["Influences"], + ("Interest", "Assessment"): ["Influences"], + ("Emotion", "Issue"): ["Influences"], + ("Emotion", "Assessment"): ["Influences"], + ("Assessment", "Claim"): ["Influences"], + ("Assessment", "Issue"): ["Influences"], + ("Assessment", "Matter"): ["Influences"], + + ("Assessment", "InformationGap"): ["IdentifiesGap"], + ("Issue", "InformationGap"): ["IdentifiesGap"], + ("Claim", "InformationGap"): ["IdentifiesGap"], + + ("Document", "InformationGap"): ["AddressesGap"], + ("Evidence", "InformationGap"): ["AddressesGap"], + ("FactAssertion", "InformationGap"): ["AddressesGap"], + ("Authority", "InformationGap"): ["AddressesGap"], + + ("Assessment", "FactAssertion"): ["Assesses"], + ("Assessment", "LegalAssertion"): ["Assesses"], + ("Assessment", "Interpretation"): ["Assesses"], + ("Assessment", "Evidence"): ["Assesses"], + ("Assessment", "Claim"): ["Assesses"], + ("Assessment", "FinancialItem"): ["Assesses"], + + ("Entity", "Entity"): ["RELATES_TO"], +} + + +# --------------------------------------------------------------------------- +# Freitextanweisung für das LLM beim Extrahieren +# --------------------------------------------------------------------------- + +EXTRACTION_INSTRUCTIONS = """ +Du analysierst Dokumente einer deutschen Rechtsanwaltskanzlei im Zivilrecht. + +Extrahiere nur Informationen, die für anwaltliche Arbeit sinnvoll sind, insbesondere +für Sachverhaltsaufklärung, Schriftsatzarbeit, Beweisführung, Anspruchsprüfung, +Vergleichsstrategie, Fristenkontrolle und Fallbewertung. + +Wichtige Grundregeln: + +1. Trenne strikt zwischen Tatsachen, Rechtspositionen und Auslegungen. + - FactAssertion: + Tatsachenbehauptungen über reale oder prozessuale Tatsachen. + Beispiele: + * "X unterzeichnete am 15.11.2020 den Dienstvertrag." + * "Die Rechnung über 12.500 EUR wurde am 04.02.2023 bezahlt." + * "Die Gegenseite erhielt das Schreiben am 18.01.2024." + - LegalAssertion: + Rechtsbehauptungen oder rechtliche Schlussfolgerungen. + Beispiele: + * "Der Vertrag ist wirksam." + * "Die Forderung ist verjährt." + * "Die Kündigung war unwirksam." + - Interpretation: + Auslegung von Vertrag, Klausel, Norm, Erklärung, Verhalten oder Kommunikation. + Beispiele: + * "§ 5 des Vertrags ist als Haftungsbegrenzung auszulegen." + * "Die E-Mail vom 12.03. ist als Kündigungserklärung zu verstehen." + +2. Trenne strikt zwischen Evidence und Authority. + - Evidence: + Beweismittel für Tatsachen, z.B. Urkunden, E-Mails, Zeugen, Gutachten, Rechnungen, Fotos, Logs. + - Authority: + Rechtliche Autoritäten, z.B. Gesetze, Gerichtsentscheidungen, Vertragsklauseln, Vertragsbestimmungen. + +3. Extrahiere Interests und Emotions nur, wenn sie im Text ausdrücklich erkennbar + oder aus dem Kontext mit hoher Plausibilität ableitbar sind. + - Interest: + wirtschaftliche, persönliche, strategische oder reputationsbezogene Interessen. + Beispiele: + * "Mandant will schnelle außergerichtliche Einigung." + * "Gegenseite will Präzedenzfall vermeiden." + - Emotion: + emotional relevante Lage oder Haltung. + Beispiele: + * "Mandant ist verärgert." + * "Gegenseite wirkt nicht vergleichsbereit." + Wenn unklar, lieber nicht extrahieren. + +4. Extrahiere Issue-Knoten für zentrale Streitfragen oder Problemfelder. + Beispiele: + - "Zugang der Kündigung" + - "Wirksamkeit der AGB-Klausel" + - "Schadenshöhe" + - "Verjährung" + - "Aktivlegitimation" + +5. Extrahiere Claim-Knoten für Ansprüche, Einreden, Einwendungen, Gegenansprüche + oder prozessuale Anträge. + Beispiele: + - Kaufpreisforderung + - Schadensersatzanspruch + - Einrede der Verjährung + - Aufrechnung + - Feststellungsantrag + +6. Extrahiere FinancialItem-Knoten für Zahlungen, Rechnungen, Schadenspositionen, + Zinsen, Kosten oder sonstige Geldpositionen, soweit konkret erkennbar. + +7. Extrahiere Deadlines nur, wenn eine Frist oder ein konkreter Termin deutlich genannt ist. + Beispiele: + - Berufungsfrist + - gerichtliche Erwiderungsfrist + - Zahlung fällig zum 31.03.2024 + - Termin zur mündlichen Verhandlung + +8. Extrahiere InformationGap nur, wenn im Text ausdrücklich oder sinngemäß erkennbar ist, + dass etwas fehlt oder noch beschafft/geklärt werden muss. + Beispiele: + - fehlende Originalvollmacht + - fehlender Zahlungsnachweis + - unklare Zustellung + - fehlende Schadensberechnung + +Beachte bei der Extraktion: +- Nutze klare, atomare Informationen statt zusammengesetzter Sätze. +- Verwende für assertion_text und ähnliche Textfelder möglichst knappe, präzise Formulierungen. +- Datumsangaben nach Möglichkeit als ISO-Datum oder ISO-Zeitpunkt extrahieren. +- Extrahiere keine strafrechtsspezifischen Rollen oder Konzepte, wenn sie im zivilrechtlichen Kontext nicht relevant sind. +- Vermeide Dubletten; identische oder offensichtlich gleiche Entitäten möglichst zusammenführen. +- Wenn eine Information nur behauptet, aber bestritten ist, extrahiere sie trotzdem, aber mit passendem Status. +- Widersprüche zwischen Tatsachenbehauptungen, Rechtspositionen oder Auslegungen sollen möglichst als Contradicts-Beziehungen erfasst werden. +- Wenn eine Tatsachenbehauptung durch ein Beweismittel gestützt wird, nutze SupportsFact. +- Wenn eine Rechtsbehauptung durch Normen, Rechtsprechung oder Vertragsklauseln gestützt wird, nutze SupportsLegalPosition. +- Wenn eine Auslegung durch Wortlaut, Kontext, Parteiwillen oder Rechtsprechung gestützt wird, nutze SupportsInterpretation. +- Verknüpfe relevante Elemente mit Issues und Claims, wenn der Zusammenhang aus dem Text erkennbar ist. +- Verknüpfe Actors mit Matters über HasRole, wenn die Rolle im Mandat oder Verfahren erkennbar ist. + +Nutze dieses Schema streng und bevorzuge juristisch brauchbare Präzision vor Vollständigkeit. +""".strip() \ No newline at end of file diff --git a/services/graphiti_tracer.py b/services/graphiti_tracer.py new file mode 100644 index 0000000..0652284 --- /dev/null +++ b/services/graphiti_tracer.py @@ -0,0 +1,132 @@ +"""Langfuse-Tracer für Graphiti Knowledge-Graph. + +Implementiert das graphiti_core.tracer.Tracer-Interface und leitet +alle Graphiti-Spans an Langfuse weiter. + +Konfiguration (Umgebungsvariablen): + LANGFUSE_PUBLIC_KEY – Langfuse Public Key (Pflicht) + LANGFUSE_SECRET_KEY – Langfuse Secret Key (Pflicht) + LANGFUSE_HOST – optional, Standard: https://cloud.langfuse.com + +Wenn LANGFUSE_PUBLIC_KEY nicht gesetzt ist, wird ein NoOp-Tracer zurückgegeben. +""" +from __future__ import annotations + +import os +from contextlib import contextmanager +from typing import Any, Generator, Optional + +from graphiti_core.tracer import NoOpSpan, NoOpTracer, Tracer, TracerSpan + + +class LangfuseTracerSpan(TracerSpan): + """Wrapper um einen aktiven Langfuse-Span.""" + + def __init__(self, langfuse_client: Any) -> None: + self._lf = langfuse_client + + def add_attributes(self, attributes: dict[str, Any]) -> None: + """Schreibt Graphiti-Attribute als Langfuse-Metadata.""" + try: + self._lf.update_current_span(metadata=attributes) + except Exception: + pass + + def set_status(self, status: str, description: str | None = None) -> None: + """Übersetzt Graphiti-Status in den Langfuse-Level.""" + try: + level = "ERROR" if status == "error" else "DEFAULT" + self._lf.update_current_span(level=level, status_message=description) + except Exception: + pass + + def record_exception(self, exception: Exception) -> None: + """Schreibt Exception als ERROR in den aktuellen Span.""" + try: + self._lf.update_current_span( + level="ERROR", + status_message=f"{type(exception).__name__}: {exception}", + ) + except Exception: + pass + + +class LangfuseTracer(Tracer): + """Graphiti-Tracer-Implementierung auf Basis von Langfuse v3. + + Jeder Graphiti-Span wird als Langfuse-Observation (type='span') + unter dem konfigurierten Präfix verschachtelt. + """ + + def __init__( + self, + langfuse_client: Any, + span_prefix: str = "graphiti", + ) -> None: + """ + Args: + langfuse_client: Initialisierter Langfuse()-Client. + span_prefix: Präfix für alle Span-Namen (z. B. "graphiti"). + """ + self._lf = langfuse_client + self._prefix = span_prefix.rstrip(".") + + @contextmanager + def start_span(self, name: str) -> Generator[LangfuseTracerSpan | NoOpSpan, None, None]: + """Startet einen Langfuse-Span für den angegebenen Graphiti-Step.""" + full_name = f"{self._prefix}.{name}" + try: + with self._lf.start_as_current_observation(name=full_name, as_type="span"): + yield LangfuseTracerSpan(self._lf) + except Exception: + yield NoOpSpan() + + +def build_langfuse_tracer( + span_prefix: str = "graphiti", + ctx: Optional[Any] = None, +) -> Tracer: + """ + Erstellt einen Langfuse-Tracer oder einen NoOp-Tracer (wenn Langfuse nicht konfiguriert). + + Args: + span_prefix: Präfix für Span-Namen. + ctx: Optionaler Motia-Context für Logging. + + Returns: + LangfuseTracer wenn LANGFUSE_PUBLIC_KEY gesetzt, sonst NoOpTracer. + """ + public_key = os.environ.get("LANGFUSE_PUBLIC_KEY") + secret_key = os.environ.get("LANGFUSE_SECRET_KEY") + host = os.environ.get("LANGFUSE_HOST") # None → cloud.langfuse.com + + def _log(msg: str) -> None: + if ctx is not None and hasattr(ctx, "logger"): + ctx.logger.info(f"[GraphitiTracer] {msg}") + else: + print(f"[GraphitiTracer] {msg}") + + if not public_key or not secret_key: + _log("LANGFUSE_PUBLIC_KEY/SECRET_KEY nicht gesetzt – Tracing deaktiviert (NoOp)") + return NoOpTracer() + + try: + from langfuse import Langfuse + + kwargs: dict[str, Any] = { + "public_key": public_key, + "secret_key": secret_key, + } + if host: + kwargs["host"] = host + + lf_client = Langfuse(**kwargs) + _log(f"Langfuse-Tracer initialisiert (host={host or 'cloud.langfuse.com'}, prefix='{span_prefix}')") + return LangfuseTracer(lf_client, span_prefix=span_prefix) + + except ImportError: + _log("langfuse nicht installiert – NoOp-Tracer") + return NoOpTracer() + except Exception as e: + _log(f"Langfuse-Init fehlgeschlagen: {e} – NoOp-Tracer") + return NoOpTracer() diff --git a/src/steps/graphiti/ingest_episode_event_step.py b/src/steps/graphiti/ingest_episode_event_step.py index 9f3e955..6e13779 100644 --- a/src/steps/graphiti/ingest_episode_event_step.py +++ b/src/steps/graphiti/ingest_episode_event_step.py @@ -10,6 +10,7 @@ from typing import Any from motia import FlowContext, queue from services.graphiti_client import get_graphiti, GraphitiError +from services.graphiti_schema import ENTITY_TYPES, EDGE_TYPES, EDGE_TYPE_MAP, EXTRACTION_INSTRUCTIONS from graphiti_core.nodes import EpisodeType @@ -81,6 +82,10 @@ async def handler(event_data: dict[str, Any], ctx: FlowContext[Any]) -> None: reference_time=reference_time, source=EpisodeType.text, group_id=rag_akten_id, + entity_types=ENTITY_TYPES, + edge_types=EDGE_TYPES, + edge_type_map=EDGE_TYPE_MAP, + custom_extraction_instructions=EXTRACTION_INSTRUCTIONS, ) episode_id = result.episode.uuid