Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
com.getorcha.aiConsolidate all AI/LLM code into com.getorcha.ai, add an agent loop powered by LangChain4j that reuses existing tool definitions to answer FP&A queries against legal entity file stores.
com.getorcha.ai
├── llm.clj ;; moved from workers.llm (same API)
├── prompts.clj ;; moved from workers (-prompt, prompt, legal-entity-prompt)
├── agent.clj ;; NEW: agent loop using raw ChatModel.chat(ChatRequest)
├── agent/
│ └── interop.clj ;; NEW: bridge tool defs → LC4j ToolSpecification + executors
└── tools.clj ;; moved from link.mcp.tools (-tool multimethod, init-registry!)
tools/
├── fpna.clj ;; defmethod registrations
├── fpna/
│ ├── data_map.clj ;; handlers (resolved legal-entity-id in context)
│ ├── list_files.clj
│ ├── excel.clj
│ ├── excel/
│ │ ├── sandbox.clj
│ │ └── functions.clj
│ └── save_dm.clj
├── docs.clj ;; defmethod registrations
│ docs/
│ ├── get.clj
│ ├── list.clj
│ ├── search.clj
│ └── line_items.clj
└── master_data.clj
LC4j is used exclusively for the new agent loop (ai.agent). The existing ai.llm/generate HTTP-based path is untouched — the 12+ worker pipeline callers keep using it. Two separate LLM interaction paths coexist without interference.
Dependencies added to deps.edn:
dev.langchain4j/langchain4j {:mvn/version "1.12.2"}
dev.langchain4j/langchain4j-anthropic {:mvn/version "1.12.2"}
dev.langchain4j/langchain4j-google-ai-gemini {:mvn/version "1.12.2"}
The agent loop uses ChatModel.chat(ChatRequest) directly rather than AiServices with definterface. This gives full control over:
max-iterations)The loop is ~30 lines of Clojure. AiServices would save ~10 lines but hide the iteration.
Tools are bridged from the existing -tool multimethod to LC4j format programmatically:
ToolSpecification — built from :name, :description, :inputSchema (JSON Schema → JsonObjectSchema)ToolExecutor — reify wrapping the existing :handler fn with resolved contextNo @Tool annotations, no gen-class, no Java source files. Pure Clojure interop.
Handlers currently receive {:legal-entity-ids #{uuid1 uuid2 ...}} in context and call mcp.identity/resolve-legal-entity-from-args internally to pick one.
After the move to ai.tools.*, handlers receive {:legal-entity-id uuid} — a single resolved UUID. Identity resolution becomes the MCP adapter's responsibility:
MCP request → identity resolution → handler(args, {:legal-entity-id resolved-id ...})
Agent call → already resolved → handler(args, {:legal-entity-id known-id ...})
The -tool multimethod and init-registry! move from link.mcp.tools to ai.tools. MCP becomes a transport adapter that reads from the ai registry and adds:
graph TB
subgraph "com.getorcha.ai"
AI_LLM[ai.llm<br/>generate, parse-json-response]
AI_PROMPTS[ai.prompts<br/>-prompt, prompt, legal-entity-prompt]
AI_TOOLS[ai.tools<br/>-tool multimethod, init-registry!]
AI_AGENT[ai.agent<br/>run — agent loop]
AI_INTEROP[ai.agent.interop<br/>build-tool-map]
subgraph "ai.tools.*"
FPNA_HANDLERS[fpna handlers<br/>data-map, list-files, excel, save-dm]
DOCS_HANDLERS[docs handlers<br/>get, list, search, line-items]
MD_HANDLER[master-data handler]
end
end
subgraph "com.getorcha.link.mcp"
MCP_TOOLS[mcp.tools<br/>thin adapter: scope check + identity resolution]
MCP_HTTP[mcp.http<br/>JSON-RPC transport]
MCP_IDENTITY[mcp.identity<br/>resolve-legal-entity-from-args]
end
subgraph "com.getorcha.workers.ap.*"
WORKERS[12+ pipeline modules<br/>transcription, classification, extraction, ...]
end
AI_AGENT --> AI_INTEROP
AI_INTEROP --> AI_TOOLS
AI_INTEROP -->|reify ToolExecutor| FPNA_HANDLERS
AI_INTEROP -->|reify ToolExecutor| DOCS_HANDLERS
AI_INTEROP -->|reify ToolExecutor| MD_HANDLER
AI_AGENT -->|ChatModel.chat| LC4J[LangChain4j ChatModel<br/>Anthropic / Gemini]
MCP_HTTP --> MCP_TOOLS
MCP_TOOLS --> AI_TOOLS
MCP_TOOLS -->|wraps with identity resolution| MCP_IDENTITY
MCP_TOOLS --> FPNA_HANDLERS
MCP_TOOLS --> DOCS_HANDLERS
MCP_TOOLS --> MD_HANDLER
WORKERS --> AI_LLM
WORKERS --> AI_PROMPTS
FPNA_HANDLERS --> FILE_STORE[link.mcp.file-store]
sequenceDiagram
participant Caller
participant ai.agent
participant ChatModel
participant ai.agent.interop
participant ai.tools.*
Caller->>ai.agent: run(context, legal-entity, model-config)
ai.agent->>ai.agent.interop: build-tool-map(context, legal-entity-id)
ai.agent.interop-->>ai.agent: {ToolSpecification → ToolExecutor}
ai.agent->>ChatModel: chat(ChatRequest[system + prompt + tool-specs])
loop until no tool calls or max-iterations
ChatModel-->>ai.agent: ChatResponse with ToolExecutionRequests
ai.agent->>ai.agent.interop: execute each tool via ToolExecutor
ai.agent.interop->>ai.tools.*: handler(args, context)
ai.tools.*-->>ai.agent.interop: result string
ai.agent->>ChatModel: chat(ChatRequest[history + ToolExecutionResultMessages])
end
ChatModel-->>ai.agent: final text response
ai.agent-->>Caller: {:text "..." :iterations N :tool-calls [...] :usage {...}}
ai.agent/run(defn run
"Runs an agent loop with tool access against a legal entity's data.
Arguments:
- context: System context {:db-pool pool :s3-client s3 ...}
- legal-entity: Legal entity map {:legal-entity/id uuid ...}
- model-config: {:provider :anthropic|:google
:model \"claude-sonnet-4-20250514\"
:api-key \"...\"
:prompt \"What were the top cost centers by spend?\"
:system \"You are a financial analyst...\" ;; optional
:max-iterations 10} ;; optional, default 10
Returns:
{:text \"The top 5 cost centers were...\"
:iterations 3
:tool-calls [{:name \"orcha-fpna-excel\" :args {...} :result \"...\"}]
:usage {:input-tokens N :output-tokens N}}"
[context legal-entity model-config]
...)
ai.agent.interop/build-tool-map(defn build-tool-map
"Converts registered ai.tools/-tool definitions to LC4j format.
Returns a java.util.Map of {ToolSpecification → ToolExecutor}
with each executor calling the tool handler with the resolved
legal-entity-id in context."
[context legal-entity-id]
...)
| From | To |
|---|---|
com.getorcha.workers.llm |
com.getorcha.ai.llm |
com.getorcha.workers/{-prompt,prompt,legal-entity-prompt} |
com.getorcha.ai.prompts |
com.getorcha.link.mcp.tools (multimethod + registry) |
com.getorcha.ai.tools |
com.getorcha.link.mcp.tools.fpna |
com.getorcha.ai.tools.fpna |
com.getorcha.link.mcp.tools.fpna.* (handlers + excel sandbox) |
com.getorcha.ai.tools.fpna.* |
com.getorcha.link.mcp.tools.docs |
com.getorcha.ai.tools.docs |
com.getorcha.link.mcp.tools.docs.* |
com.getorcha.ai.tools.docs.* |
com.getorcha.link.mcp.tools.master-data |
com.getorcha.ai.tools.master-data |
workers.ap.ingestion.*, workers.ap.matching.*, etc.) — llm alias → ai.llm, workers/prompt → ai.prompts/promptadmin.http.tenants.prompt-customizations — references ai.prompts/-promptlink.mcp.tools — thin adapter over ai.toolslink.mcp.http — calls ai.tools/init-registry!link.mcp.file-store and backendslink.mcp.resources.*link.mcp.middleware, link.mcp.identityIntegration tests that maximize coverage without exhaustive edge cases:
build-tool-map converts all registered tool defs to valid LC4j ToolSpecification objects, executors call through to handlersai.llm/generate to LC4j ChatModel (existing HTTP path stays)link.mcp.resources.fpna stays)