Sie sind hier

Connecting LangChain to Alfresco Without Writing Another REST Loader

You already have a LangChain application. The content you need is in Alfresco Content Services.

At first, it is tempting to write a small loader around the Alfresco REST API: search a couple of nodes, download the content, extract text, add the chunks to a vector store.

That works for the first demo. Then the edge cases arrive.

You need the path, the site, the content type, custom properties, aspects, checked-out state, versions, permissions, and security markings. For some information, like Type and Aspect definitions, there is no matching Alfresco REST API. Your production repository may also contain custom or exotic file formats for which there is no useful Python implementation. Alfresco Content Services may already be configured to handle those formats, but your LangChain application does not know that.

This is where a simple REST loader quickly turns into a rabbit hole.

OpenDMA provides another boundary. The Alfresco adaptor maps Alfresco into the OpenDMA model. The LangChain integration talks to the OpenDMA REST endpoint and returns normal LangChain Document objects. The same application code can then use the OpenDMA model instead of spreading Alfresco REST details across the RAG pipeline.

Unlike CMIS, OpenDMA is not only an abstraction layer. It is a framework for putting functionality between the repository and the application: auditing, identity translation, content transformations, SIEM integration, and repository federation belong in that layer, not in every LangChain script.

The examples below assume that an OpenDMA endpoint for Alfresco is already available at:

http://localhost:7070/opendma/alf

Install the LangChain OpenDMA Integration with:

pip install langchain-opendma

The examples use admin/admin only to keep the snippets short.

A runnable version of the examples is available here: https://github.com/xaldon/opendma-demo-alfresco-langchain-basics

Load Alfresco Content Into LangChain

Use AlfrescoLoader when you want to ingest Alfresco content into a LangChain application, for example to build a vector store.

from langchain_opendma import AlfrescoLoader
from langchain_opendma.content_handlers import UnstructuredLoaderContentHandler

handler = UnstructuredLoaderContentHandler(
    chunking_strategy="by_title",
    max_characters=4000,
    new_after_n_chars=3000,
    combine_text_under_n_chars=1000,
)

loader = AlfrescoLoader(
    endpoint="http://localhost:7070/opendma/alf",
    username="admin",
    password="admin",
    repository_id="Alfresco",
    sites=["swsdp"],
    content_handlers=[handler],
)

documents = loader.load()

Each returned LangChain Document contains extracted text in page_content and OpenDMA metadata in metadata.

for document in documents[:5]:
    print(document.metadata.get("opendma:Title"))
    print(document.page_content[:200])

The AlfrescoLoader is a convenience loader on top of the generic OpenDMALoader. It adds Alfresco defaults and Alfresco-specific concepts like sites. With sites=["swsdp"], the loader searches the Alfresco site with that short name and loads the document nodes below the matching site folder.

The content handler controls how binary content becomes text. The default handler processes plain text. For Office documents, PDFs, HTML, and similar formats, use an Unstructured or Docling handler.

When the content handler chunks a document, one Alfresco document can become multiple LangChain Document objects. This is the usual shape for vector-store ingestion.

from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
vector_store = InMemoryVectorStore(embeddings)

vector_store.add_documents(documents)

In production, the indexing job normally runs for new and changed documents and writes to a persistent vector store. The important part is not the vector store used in this snippet. The important part is that the documents still carry Alfresco and OpenDMA metadata after loading.

The sample repository contains
a small CLI version of this flow. It loads one Alfresco site, builds an in-memory vector store, and
then lets you ask questions against it.

Keep The Metadata

The metadata is not decorative.

After loading content from Alfresco nodes, the returned documents can contain metadata such as:

print(document.metadata.get("opendma:Title"))
print(document.metadata.get("opendma:CheckedOut"))
print(document.metadata.get("alfresco:Site"))
print(document.metadata.get("alfresco:Path"))
print(document.metadata.get("alfresco:cm:name"))
print(document.metadata.get("alfresco:cm:creator"))
print(document.metadata.get("alfresco:cm:accessed"))

This matters as soon as the repository stops looking like a small demo. A real Alfresco system can contain meeting notes for many teams, several generations of project documents, contracts, specifications, scanned content, and old versions. A plain similarity search across everything can return text chunks that are close to the question but from the wrong site or the wrong context.

You can use the metadata for filtering:

results = vector_store.similarity_search(
    "state of localisation of the new website design",
    k=10,
    filter=lambda doc: (
        doc.metadata.get("alfresco:Site") == "swsdp"
        and doc.metadata.get("opendma:CheckedOut") is False
    ),
)

This article is not about metadata-aware retrieval as a complete pattern. That deserves its own article. The basic point is simpler: preserve repository metadata from the start. If your loader throws it away, you have already removed information that the retrieval step will need later.

Search Alfresco Live With AlfrescoRetriever

A vector store is not always the right first choice.

Building one means extracting content, chunking it, calculating embeddings, storing the chunks, keeping them up to date, and securing a second copy of repository text. That can be useful, but it is not free.

There is also a retrieval quality issue in the other direction. Similarity search is good when the wording of the user question differs from the wording of the document. It is less appropriate when exact terms matter. Product codes, customer numbers, invoice IDs, claim numbers, or part numbers should not be "semantically similar". They should match.

For these cases, use AlfrescoRetriever to run a live full-text search through OpenDMA:

from langchain_opendma import AlfrescoRetriever
from langchain_opendma.content_handlers import UnstructuredLoaderContentHandler

handler = UnstructuredLoaderContentHandler(
    chunking_strategy="by_title",
    max_characters=4000,
    new_after_n_chars=3000,
    combine_text_under_n_chars=1000,
)

retriever = AlfrescoRetriever(
    endpoint="http://localhost:7070/opendma/alf",
    username="admin",
    password="admin",
    repository_id="Alfresco",
    sites=["swsdp"],
    content_handlers=[handler],
    k=8,
)

documents = retriever.invoke("meeting notes")

AlfrescoRetriever implements LangChain's retriever API. The input string is converted into an Alfresco AFTS full-text query, including escaping of special characters. If sites is set, the query is restricted to those Alfresco sites.

The result is still a list of LangChain Document objects.

Because the search runs through Alfresco Content Services via the OpenDMA endpoint, repository access control, governance rules, and security markings remain in the live retrieval path. This is different from querying a separate vector store that contains copied text.

For some applications, this is enough. For others, live full-text search and vector search can both be useful, depending on the question.

The sample repository also contains
the same idea as a small live-search command.

Give An Agent Alfresco Tools

Sometimes the agent should not only retrieve chunks. It needs to inspect the repository.

For example:

  • Which Alfresco sites exist?
  • Which documents are in this folder?
  • What type and aspects does this object have?
  • Is the answer already in metadata?
  • Which document text should be read?

For that, use AlfrescoToolkit.

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_opendma import AlfrescoToolkit
from langchain_opendma.content_handlers import DoclingLoaderContentHandler

toolkit = AlfrescoToolkit(
    endpoint="http://localhost:7070/opendma/alf",
    username="admin",
    password="admin",
    repository_id="Alfresco",
    content_handlers=[DoclingLoaderContentHandler()],
)

agent = create_agent(
    model=init_chat_model("openai:gpt-4o-mini", temperature=0),
    tools=toolkit.get_tools(),
    system_prompt=(
        "You answer questions using documents stored in Alfresco. "
        "Use repository tools to find documents and inspect metadata before "
        "reading document text."
    ),
)

The toolkit exposes tools such as:

  • alfresco_list_sites
  • opendma_list_children
  • opendma_get_metadata
  • opendma_describe_class
  • opendma_search
  • opendma_read_text

This gives the agent a different way to work. It can list sites, browse folders, inspect metadata, search, and read selected text chunks.

Reading document text is more expensive than listing children or reading metadata. A good agent prompt should make that visible. Often the agent should first locate candidate documents, inspect their metadata, and only then read text.

The toolkit example in the sample repository runs this as an interactive agent.

This is still the basic toolkit usage. More elaborate agentic RAG flows, where the agent decides how often to search and how to assess retrieved context, are a separate topic.

Why Not Start With cmislib Or Direct REST?

CMIS used to be the obvious abstraction to mention around ECM systems. For new LangChain work, I would not start there.

Apache Chemistry's cmislib project is retired. Its documentation is visibly stale. It may still be useful in old systems, but it is not a good foundation for a new LangChain integration.

Calling Alfresco REST directly is different. It is often the right choice for a small script with a narrow job. The problem starts when that small script becomes the content layer for an LLM application.

Then you need to model:

  • metadata
  • Type and Aspect definitions
  • paths and sites
  • versions and checked-out state
  • permissions and security markings
  • content conversion
  • query escaping
  • errors and partial failures

For custom and exotic file formats, you may also end up rebuilding in Python what Alfresco Content Services is already configured to handle.

OpenDMA keeps the Alfresco-specific work in the adaptor and integration layer. The LangChain application sees Document objects, retrievers, and tools.

OpenDMA also models renditions. That matters because production Alfresco repositories often already know how to turn custom formats into PDF or extractable text. A LangChain application should not become the place where all of that repository-specific conversion logic is rebuilt.

Enterprise Notes

The OpenDMA endpoint is also the place where enterprise concerns can be handled without putting them into every LangChain application:

  • auditing repository requests made by an agent
  • identity forwarding or token-based sessions
  • identity translation
  • RBAC
  • SIEM integration
  • federation across multiple ECM systems
  • repository-side rendition support

Those are adaptor, middleware, and deployment concerns. The LangChain code should stay small.

Which OpenDMA Integration Surface To Use

Use AlfrescoLoader when you want to ingest Alfresco content into a vector store or another knowledge base.

Keep the Alfresco metadata in the LangChain documents from the start. Later filtering and metadata-aware retrieval depend on it.

Use AlfrescoRetriever when live Alfresco full-text search is preferable to a second indexed copy of repository content.

Use AlfrescoToolkit when an agent needs to navigate Alfresco, inspect metadata, search, and read selected text chunks.

Further reading: