Metadata-Version: 2.4
Name: agentloop-py-langchain
Version: 0.1.0
Summary: LangChain integration for AgentLoop — auto-logs turns to the review queue and provides a memory injection Runnable for retrieval.
Project-URL: Homepage, https://github.com/martinembon/agentloop-sdk
Project-URL: Repository, https://github.com/martinembon/agentloop-sdk
Author: AgentLoop
License: MIT
Keywords: agentloop,agents,callback,langchain,llm,memory,rag
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Requires-Dist: agentloop-py>=0.1.3
Requires-Dist: langchain-core>=0.1.0
Provides-Extra: dev
Requires-Dist: langchain-core>=0.1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# agentloop-py-langchain

LangChain integration for [AgentLoop](https://agentloop.dev) — adds memory
retrieval before your LLM calls and turn logging after, with a single
chain composition step and a single callback.

```bash
pip install agentloop-py agentloop-py-langchain
```

The install name is `agentloop-py-langchain` but you import from
`agentloop_langchain` (same `pip install bs4` / `from bs4 import ...`
convention as the rest of the AgentLoop SDK).

---

## What this gives you

A LangChain user dropping AgentLoop into an existing chain gets two
things:

1. **Memory retrieval** — before the LLM call, search past corrections
   for facts relevant to the user's question, and inject them into the
   prompt automatically. This is **the part that makes your agent
   smarter immediately** when reviewers correct it.
2. **Turn logging** — after every LLM call, post the (question, response)
   pair to the AgentLoop review queue so reviewers can correct anything
   the agent got wrong.

Both halves work in sync chains (`chain.invoke`) and async chains
(`chain.ainvoke`).

---

## Why two pieces and not one?

Other AgentLoop integrations (OpenAI, Anthropic) wrap the SDK call
itself, so a single `wrap_openai(client, loop=...)` does both halves
invisibly. **LangChain doesn't allow that pattern.** Its callback
system is observation-only — by the time `on_llm_start` fires, the
prompt is already finalized and being sent. We can't sneak retrieved
facts in.

So we split the work:

- **`AgentLoopMemoryInjector`** runs *upstream* in the chain (before the
  prompt template), where it can actually shape the LLM input.
- **`AgentLoopCallbackHandler`** runs *as a callback* on the LLM,
  observing what was sent and what came back, then posting to AgentLoop.

The two-step setup is more code than the OpenAI wrapper, but it's still
fewer than 5 lines on top of an existing LangChain chain. And it's
honest about what LangChain's design lets us do — no fragile prompt
mutation tricks that would break on minor LangChain version bumps.

---

## The complete pattern

```python
from agentloop import AgentLoop
from agentloop_langchain import (
    AgentLoopMemoryInjector,
    AgentLoopCallbackHandler,
)
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

loop = AgentLoop(api_key="ak_live_...")

# Step 1: a prompt template that has a slot for retrieved facts.
prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a helpful assistant.\n\n"
     "Trusted facts from past corrections:\n{agentloop_memories}"),
    ("user", "{question}"),
])

# Step 2: the chain. Three pieces composed with `|`:
chain = (
    # 2a. Retrieve relevant memories — runs first
    AgentLoopMemoryInjector(loop=loop, query_field="question")
    # 2b. Format the prompt with the retrieved facts
    | prompt
    # 2c. Call the LLM, with the callback that logs the turn afterwards
    | ChatOpenAI(model="gpt-4o").with_config(
        callbacks=[AgentLoopCallbackHandler(loop=loop)],
    )
)

# Step 3: use the chain normally.
result = chain.invoke({"question": "What's the Pix limit at night?"})
```

That's the full pattern. Three changes to a stock LangChain chain:

- One extra step at the start (`AgentLoopMemoryInjector | ...`)
- One extra placeholder in the prompt (`{agentloop_memories}`)
- One callback on the LLM (`callbacks=[AgentLoopCallbackHandler(...)]`)

---

## What actually happens, step-by-step

This is the part worth understanding because it's where the value lands.

### Before the call: memory retrieval

When you `chain.invoke({"question": "What's the Pix limit at night?"})`:

1. The injector receives `{"question": "What's the Pix limit at night?"}`.
2. It calls `loop.search(query="What's the Pix limit at night?", limit=3)`.
3. AgentLoop's backend semantically searches your org's memories
   (corrections reviewers have approved over time) and returns the
   most relevant matches. For example:
   - *"The Pix nighttime limit is R$1,000 between 8pm and 6am."*
   - *"Pix limits reset at 6am, not midnight."*
4. The injector adds those to the input as a formatted string under
   the `agentloop_memories` key:

   ```python
   {
     "question": "What's the Pix limit at night?",
     "agentloop_memories": "- The Pix nighttime limit is R$1,000 between 8pm and 6am.\n- Pix limits reset at 6am, not midnight.",
   }
   ```

5. The prompt template renders, using `{agentloop_memories}` as a slot,
   producing this final prompt for the LLM:

   ```
   system: "You are a helpful assistant.

           Trusted facts from past corrections:
           - The Pix nighttime limit is R$1,000 between 8pm and 6am.
           - Pix limits reset at 6am, not midnight."

   user: "What's the Pix limit at night?"
   ```

6. The LLM sees this and answers correctly, even if its training data
   would have made it answer wrongly without those facts.

**This is the magic.** Every correction your reviewers make in the
AgentLoop dashboard becomes a future-tense fact that gets injected into
the next relevant prompt automatically. No fine-tuning, no retraining,
no manual prompt engineering — it just works the next time someone
asks a similar question.

### After the call: turn logging

After the LLM responds:

1. The callback handler (registered via `with_config(callbacks=[...])`)
   receives the response in its `on_llm_end` hook.
2. It pulls the question (captured at `on_llm_start`) and the response
   text out of the LLMResult.
3. It calls `loop.log_turn(question=..., agent_response=...)` which
   posts the turn to the AgentLoop review queue.
4. Reviewers see the turn in the dashboard. If the LLM got it wrong,
   they correct it. The correction becomes a memory. The next time a
   similar question comes through, that memory gets retrieved in step 3
   above. The loop closes.

---

## Per-call options

Pass user-specific or call-specific data via LangChain's `metadata`:

```python
chain.invoke(
    {"question": "What's the Pix limit at night?"},
    config={"metadata": {"agentloop": {
        "user_id": "u_42",
        "session_id": "sess_xyz",
        "signals": {"thumbs_down": True},
        "tags": ["pix", "limits"],
    }}},
)
```

Recognized keys: `user_id`, `session_id`, `signals` (dict), `tags`
(list), `metadata` (dict, free-form), `skip` (bool — skip logging this
turn entirely).

You can also set defaults at construction:

```python
AgentLoopCallbackHandler(loop=loop, user_id="u_42", tags=["beta"])
```

Per-call values override construction-time defaults.

---

## Async chains

Use `AsyncAgentLoopCallbackHandler` and pass `AsyncAgentLoop` (from
`agentloop.aio`) to the injector:

```python
from agentloop.aio import AsyncAgentLoop
from agentloop_langchain import (
    AgentLoopMemoryInjector,
    AsyncAgentLoopCallbackHandler,
)

async with AsyncAgentLoop(api_key="ak_...") as loop:
    chain = (
        AgentLoopMemoryInjector(loop=loop, query_field="question")
        | prompt
        | ChatOpenAI(model="gpt-4o").with_config(
            callbacks=[AsyncAgentLoopCallbackHandler(loop=loop)],
        )
    )
    result = await chain.ainvoke({"question": "..."})
```

If you accidentally pass a sync `AgentLoop` to either async component,
they'll detect that and dispatch the network calls to a thread-pool
executor so the event loop isn't blocked. Works either way; passing
an `AsyncAgentLoop` is just slightly more efficient.

---

## Customization

### Where memories appear in the prompt

The injector writes to `input["agentloop_memories"]` by default. Use
that key in your prompt template wherever you want the retrieved facts
to appear. To use a different key:

```python
AgentLoopMemoryInjector(loop=loop, query_field="user_question",
                        output_field="known_facts")
```

### How memories are formatted

The default formatter renders memories as a bulleted list of `.fact`
strings, or `(none yet)` when the search returns nothing. Override:

```python
def my_formatter(memories):
    if not memories:
        return ""
    return "Reference material:\n" + "\n".join(
        f"  • {m.fact}" for m in memories
    )

AgentLoopMemoryInjector(loop=loop, query_field="q",
                        format_memories=my_formatter)
```

### Filtering search by tags / user

```python
AgentLoopMemoryInjector(
    loop=loop,
    query_field="question",
    tags=["pix"],              # only memories tagged "pix"
    user_id_field="user_id",   # search per-user when input has user_id
    limit=5,                   # retrieve top 5 instead of default 3
)
```

### Imperative usage (no LCEL)

If your code doesn't compose with `|`:

```python
from agentloop_langchain import inject_memories

enriched = inject_memories({"question": q}, loop=loop)
# enriched["agentloop_memories"] is the formatted string
prompt_text = template.format(**enriched)
response = llm.invoke(prompt_text)
loop.log_turn(question=q, agent_response=response.content)
```

---

## Failure mode

If AgentLoop is unreachable or returns an error:

- The injector returns the input unchanged with `agentloop_memories`
  set to `(none yet)`. The chain continues; the LLM just doesn't get
  retrieved facts for that call.
- The callback handler swallows the error and logs a warning. The
  chain's response goes back to the user normally; that turn just
  doesn't end up in the review queue.

Both behaviors are deliberate. AgentLoop is a value-add layer; if it's
having a bad day, your agent should still respond.

---

## Compatibility

- Python 3.9+
- `langchain-core` 0.1.0+
- Works with any LLM that has a LangChain integration (`langchain-openai`,
  `langchain-anthropic`, `langchain-google-genai`, `langchain-cohere`,
  Ollama, vLLM, etc.) — we depend on the abstract callback interface,
  not any specific provider.

---

## License

MIT
