Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion py/gaas_gpt_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,32 @@ class Config:

allow_population_by_field_name = True

def bind_tools(
self,
tools: List[Any],
*,
tool_choice: Optional[Any] = None,
**kwargs: Any,
):
"""Bind LangChain tools onto the chat model.

Tools are converted to the OpenAI function-schema shape SEMOSS
already normalizes on (see ``semoss_base.semoss_message_builder``
for the canonical tool_call dict). This is what makes the model
usable inside ``langgraph.prebuilt.create_react_agent`` and any
downstream framework that speaks LangChain's tool-calling
protocol.
"""
from langchain_core.utils.function_calling import (
convert_to_openai_tool,
)

formatted = [convert_to_openai_tool(t) for t in tools]
bind_kwargs: Dict[str, Any] = {"tools": formatted, **kwargs}
if tool_choice is not None:
bind_kwargs["tool_choice"] = tool_choice
return self.bind(**bind_kwargs)

def _generate(
self,
messages: List[BaseMessage],
Expand All @@ -746,15 +772,61 @@ def _generate(

return self._create_chat_result(response=response[0])

def _extract_tool_calls(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Return LangChain-shaped tool_calls from a raw model response.

Handles the three shapes SEMOSS providers commonly return:
openai-style ``tool_calls``, anthropic-style ``tool_use``
blocks, and gemini-style ``function_calls``. Returns ``[]``
when nothing tool-shaped is present.
"""
import json as _json

raw = (
response.pop("tool_calls", None)
or response.pop("tool_uses", None)
or response.pop("function_calls", None)
)
if not raw:
return []

normalized: List[Dict[str, Any]] = []
for i, item in enumerate(raw):
fn = item.get("function") or item
name = fn.get("name") or item.get("name")
args = fn.get("arguments") or item.get("input") or {}
if isinstance(args, str):
try:
args = _json.loads(args)
except Exception:
args = {"_raw": args}
if not name:
continue
normalized.append(
{
"name": name,
"args": args,
"id": str(item.get("id") or f"call_{i}"),
"type": "tool_call",
}
)
return normalized

def _create_chat_result(self, response: Dict[str, Any]) -> ChatResult:
generations = []

message = response.pop("response", "")
generation_info = dict()
if "logprobs" in response.keys():
generation_info["logprobs"] = response.pop("logprobs", {})

tool_calls = self._extract_tool_calls(response)
ai_kwargs: Dict[str, Any] = {"content": message}
if tool_calls:
ai_kwargs["tool_calls"] = tool_calls

gen = ChatGeneration(
message=AIMessage(content=message),
message=AIMessage(**ai_kwargs),
generation_info=generation_info,
)

Expand Down
96 changes: 96 additions & 0 deletions py/genai_client/agents/langgraph_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# SEMOSS ↔ LangGraph adapter

Materialize a SEMOSS workspace agent config as a LangGraph
[`CompiledGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#compiledstategraph)
so anything that consumes LangGraph — LangSmith, LangGraph Studio, LangServe,
composition into another graph — can consume a SEMOSS-authored agent.

## Quickstart

```python
from genai_client.agents.langgraph_agent import SemossAgent

agent = SemossAgent.from_workspace(
"93c85f32-1023-425d-8167-14111f26ceb4",
access_key="...",
secret_key="...",
room_id="babae1e3-42cb-490d-a622-c06e6a59da54",
)

result = agent.invoke(
{"messages": [{"role": "user", "content": "summarize the recent news"}]}
)

for chunk in agent.stream({"messages": [{"role": "user", "content": "..."}]}):
print(chunk)
```

The returned object is a stock LangGraph `CompiledGraph` — anything you would
do with `create_react_agent(...)` works identically here.

## What the adapter maps

| SEMOSS | LangGraph |
| --- | --- |
| `WORKSPACE.system_prompt` | react agent `prompt` |
| `WORKSPACE.model_engine_id` | LangChain `BaseChatModel` via `ModelEngine.to_langchain_chat_model()` |
| `WORKSPACE.mcp[]` | `BaseTool`s via `langchain-mcp-adapters` |
| `CONFIG_JSON.subagents[]` | Child `CompiledGraph`s wrapped as delegate tools |
| `CONFIG_JSON.mode == "deep"` | Routed through `deepagents.create_deep_agent` |

## Deep mode

Set `mode="deep"` on the workspace's `CONFIG_JSON` (or override at build time)
to route through [`deepagents`](https://docs.langchain.com/oss/python/deepagents/overview).
The child gets a planning tool (TodoWrite-style), a virtual filesystem, and
its subagents materialized in deepagents' native format.

```python
agent = SemossAgent.from_workspace("...", mode="deep", ...)
```

Deep mode is entirely opt-in; workspaces without `mode` default to a plain
react agent.

## Configuration surface

`SemossAgentConfig` is a Pydantic model — use it directly when you want to
bypass the workspace fetch:

```python
from genai_client.agents.langgraph_agent import (
SemossAgent,
SemossAgentConfig,
MCPRef,
SubAgentRef,
)

cfg = SemossAgentConfig(
system_prompt="You are a careful research assistant.",
model=my_chat_model, # BaseChatModel | ModelEngine | engine_id str
mcps=[MCPRef(url="...", name="search")],
subagents=[SubAgentRef(alias="researcher", workspaceId="ddd2a191-...")],
mode="react",
access_key="...", secret_key="...", room_id="...",
)
agent = SemossAgent.from_config(cfg)
```

## External usage

`from_workspace` fetches via `semoss.Insight().run_pixel(...)` by default,
which requires running inside a SEMOSS Python runtime. For external LangGraph
apps, supply a `pixel_loader` callable that hits the SEMOSS REST endpoint:

```python
def my_loader(pixel: str) -> dict:
...

agent = SemossAgent.from_workspace("...", pixel_loader=my_loader, ...)
```

## Depth guard

`max_subagent_depth` (default 1) mirrors
`AgentConfig.SubAgentSpawnPolicy.DEFAULT_MAX_SUBAGENT_DEPTH`. Increase only
if you understand the risk of unbounded delegation.
33 changes: 33 additions & 0 deletions py/genai_client/agents/langgraph_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""SEMOSS ↔ LangGraph adapter.

Materializes a SEMOSS workspace configuration as a LangGraph
``CompiledGraph`` so that ``langgraph``, ``langsmith`` and downstream
tooling can consume a SEMOSS-authored agent without knowing SEMOSS is the
source of truth.

Usage (in-SEMOSS)::

from genai_client.agents.langgraph_agent import SemossAgent

agent = SemossAgent.from_workspace(
"93c85f32-1023-425d-8167-14111f26ceb4",
access_key="...",
secret_key="...",
room_id="babae1e3-...",
)
result = agent.invoke({"messages": [{"role": "user", "content": "hi"}]})

Deep-mode (planning tool + virtual filesystem + subagents) via
``mode="deep"`` on the workspace config or overridden at build time.
"""

from .agent import SemossAgent, build_agent
from .config import MCPRef, SemossAgentConfig, SubAgentRef

__all__ = [
"SemossAgent",
"SemossAgentConfig",
"MCPRef",
"SubAgentRef",
"build_agent",
]
Loading
Loading