Skip to content

Add agent-langgraph-scratch template (raw FastAPI) - #265

Draft
elainemwang wants to merge 13 commits into
databricks:mainfrom
elainemwang:add-agent-langgraph-scratch
Draft

Add agent-langgraph-scratch template (raw FastAPI)#265
elainemwang wants to merge 13 commits into
databricks:mainfrom
elainemwang:add-agent-langgraph-scratch

Conversation

@elainemwang

Copy link
Copy Markdown

A LangGraph agent served from a hand-written FastAPI app (no LongRunningAgentServer / AgentServer). server/app.py is the SDK-agnostic Responses surface (byte-identical to agent-openai-scratch); only the agent/wire handlers differ. In-memory background mode, in-process checkpointer, optional tracing + managed memory store.

elainemwang and others added 13 commits August 26, 2026 00:32
A LangGraph agent served from a hand-written FastAPI app (no LongRunningAgentServer / AgentServer).
server/app.py is the SDK-agnostic Responses surface (byte-identical to agent-openai-scratch);
only the agent/wire handlers differ. In-memory background mode, in-process checkpointer, optional
tracing + managed memory store.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
outbound.py now relays LangChain message dicts + text deltas as-is (no Responses conversion),
matching scratch's 'show the SDK's raw shape' intent. Input stays Responses-style.

Co-authored-by: Isaac <no-reply@databricks.com>
input list of LangChain message dicts passes straight to the agent; no to_chat_completions_input.
scratch now speaks the SDK's native shape on both ends.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
The background-run store is a non-durable stand-in slated for a durable backend, so it belongs with
the other mason plumbing. server/app.py imports BackgroundRuns from mason (this diverges its
server/app.py from agent-openai-scratch's).

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
invoke_handler(request: dict) -> dict
stream_handler(request: dict) -> AsyncGenerator[dict]

Nothing here is OpenAI- or LangGraph-specific — the agent SDK lives entirely behind those handlers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this section? It reads more like a tutorial than a template

def __init__(self) -> None:
self._runs: dict[str, dict[str, Any]] = {}

def create(self) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unless we plan to separate creation and starting

Suggested change
def create(self) -> str:
def start(self) -> str:

mlflow.update_current_trace(tags={_TRACE_NAME_TAG: "invoke_handler"})
span.set_inputs(request)
result = await invoke_handler(request)
span.set_attribute(_MESSAGE_FORMAT_ATTR, "openai")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be openai?

Comment on lines +36 to +40
outputs = [
event["message"]
async for event in stream_handler(request)
if event.get("type") == "message"
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is delegating invoke to call streaming a standard pattern?

Comment on lines +98 to +100
| Run locally | `uv run start-server` |
| Test | `uv run pytest` (hermetic; live model test runs only with a profile) |
| Deploy | `databricks apps deploy agent-langgraph-scratch --source-code-path <path>` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will these be updated with the mason CLI once it's ready?

Comment on lines +98 to +99
app.add_api_route("/invocations", _handle, methods=["POST"])
app.add_api_route("/responses", _handle, methods=["POST"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need both routes? what if we only keep invocations?

Comment on lines +27 to +33
def complete(self, response_id: str, output: dict) -> None:
self._runs[response_id] = {"status": "completed", "output": output, "error": None}

def fail(self, response_id: str, error: str) -> None:
self._runs[response_id] = {"status": "failed", "output": None, "error": error}

def get(self, response_id: str) -> dict | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will using response_id be confusing if the runtime service API may not be Responses API compatible?

app.add_api_route("/invocations", _handle, methods=["POST"])
app.add_api_route("/responses", _handle, methods=["POST"])

@app.get("/responses/{response_id}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be changed to /invocations/{invocation_id} to better decouple from Responses?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wire module is a misnomer since it's less about request/response transformation which is what's here vs raw encoding. These are pretty thin right now, can we move them into app.py?

Comment on lines +12 to +14
def get_session_id(request: dict) -> str:
"""Return the request's ``session_id`` (for multi-turn), or a fresh UUID for a new conversation."""
return str(request.get("session_id") or uuid7())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's update this to pull the session_id from the header as our recent discussion: https://databricks.slack.com/archives/C0BL69HA4DQ/p1787345723987959

Comment on lines +13 to +18
# Leave UNSET to skip tracing (local dev). To enable, set a destination AND an experiment (either
# form of each works): destination = MLFLOW_TRACKING_URI or MLFLOW_TRACING_DESTINATION;
# experiment = MLFLOW_EXPERIMENT_ID or MLFLOW_EXPERIMENT_NAME.
# MLFLOW_TRACKING_URI="databricks"
# MLFLOW_EXPERIMENT_ID=
# MLFLOW_EXPERIMENT_NAME=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be possible to create a local mlflow tracking server from the OSS code if we want this to work E2E in dev mode, let's investigate that. (Not a blocker for this PR)

@jamesbxwu jamesbxwu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'll be awesome if we can bundle a basic chat UI to mason dev so that we can show off sync/async + streaming. This doesn't block this PR and it can be a follow up.

@jamesbxwu jamesbxwu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed offline, can we move these templates for now into databricks-ai-bridge/mason/templates until we are ready to publish them?

@jamesbxwu jamesbxwu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion on folder structure: can we rename the top level server folder to runtime and potentially the following files

  • start_server.py -> main.py
  • app.py -> runtime.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants