Skip to content
Merged
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
4 changes: 2 additions & 2 deletions grafi_dev/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def run(
host: str = "127.0.0.1",
port: int = 8080,
assistant_name: str = "assistant",
is_async: bool = True,
is_sequential: bool = True,
open_browser: bool = True,
):
"""Run the assistant in *script* and launch the web UI."""
Expand All @@ -72,7 +72,7 @@ def run(

# Pass the assistant instance directly to create_app
uvicorn.run(
lambda: create_app(assistant=assistant, is_async=is_async), # type: ignore
lambda: create_app(assistant=assistant, is_sequential=is_sequential), # type: ignore
factory=True, # <─ tells Uvicorn to call it
host=host,
port=port,
Expand Down
123 changes: 83 additions & 40 deletions grafi_dev/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@
let selected = { type: null, id: null };

// build graph & interaction
let cy, wf;
let cy, wf, assistant_json;
(async () => {
assistant_json = await (await fetch("/workflow")).json();
renderYAML(panes.Info, assistant_json);
Expand All @@ -331,9 +331,8 @@
);
Object.values(wf.nodes).forEach((n) => {
elems.push({ data: { id: n.name, label: n.name, type: "node" } });
if (n.command) {
const commandName = Object.keys(n.command)[0];
const toolName = n.command[commandName].name || commandName;
if (n.tool) {
const toolName = n.tool.name;
const toolId = `${n.name}#tool#${toolName}`;

elems.push({ data: { id: toolId, label: toolName, type: "tool" } });
Expand All @@ -352,51 +351,44 @@
},
});
}
n.publish_to.forEach((t) =>
n.publish_to.forEach((topicName) =>
elems.push({
data: {
id: `${n.name}->${t.name}`,
id: `${n.name}->${topicName}`,
source: n.name,
target: t.name,
target: topicName,
},
})
);
n.subscribed_expressions.forEach((expr) => {
// Recursively extract all topics from the expression tree
function extractTopics(expression) {
const topics = [];


// If this expression has a topic field, it's the topic name (string)
if (expression.topic) {
topics.push(expression.topic);
}


// Check left side of expression (for AND/OR operations)
if (expression.left) {
if (expression.left.topic) {
topics.push(expression.left.topic);
} else {
// Recursively extract from left expression
topics.push(...extractTopics(expression.left));
}
topics.push(...extractTopics(expression.left));
}


// Check right side of expression (for AND/OR operations)
if (expression.right) {
if (expression.right.topic) {
topics.push(expression.right.topic);
} else {
// Recursively extract from right expression
topics.push(...extractTopics(expression.right));
}
topics.push(...extractTopics(expression.right));
}

return topics;
}

const allTopics = extractTopics(expr);
allTopics.forEach((topic) => {
allTopics.forEach((topicName) => {
elems.push({
data: {
id: `${topic.name}->${n.name}`,
source: topic.name,
id: `${topicName}->${n.name}`,
source: topicName,
target: n.name,
},
});
Expand Down Expand Up @@ -625,7 +617,7 @@
});

// node/tool/topic click
cy.on("tap", "node", async (evt) => {
cy.on("tap", "node", (evt) => {
// remove prior highlights
cy.elements().removeClass("highlighted");
// highlight this element
Expand All @@ -636,15 +628,53 @@
if (currentTab === "Info") {
if (typ === "tool") {
const node_name = raw.split("#tool#")[0]; // Extract node name from node_name#tool#tool_name
renderYAML(panes.Info, wf.nodes[node_name].command);
renderYAML(panes.Info, wf.nodes[node_name].tool);
} else if (typ === "node") {
renderYAML(panes.Info, wf.nodes[raw]);
} else {
renderYAML(panes.Info, wf.topics[raw]);
}
} else if (currentTab === "Event") {
showTab("Event");
await document.querySelector('[data-tab="Event"]').onclick();
// Manually trigger the event filtering and rendering
let evs = eventsCache[convSelect.value] || [];

// apply element filters
if (selected.type === "node") {
evs = evs.filter(
(e) =>
["NodeInvoke", "NodeRespond"].includes(e.event_type) &&
e.name === selected.id
);
}
if (selected.type === "tool") {
const toolName = selected.id.split("#tool#")[1];
evs = evs.filter(
(e) =>
["ToolInvoke", "ToolRespond"].includes(e.event_type) &&
e.name === toolName
);
}
if (selected.type === "topic") {
evs = evs.filter(
(e) =>
["PublishToTopic", "ConsumeFromTopic"].includes(e.event_type) &&
e.name === selected.id
);
}
if (reqSelect.value !== "All") {
evs = evs.filter(
(e) => e.invoke_context.assistant_request_id === reqSelect.value
);
}

// render according to drop‑down choice
switch (eventView.value) {
case "formed":
renderFormedEvents(evs);
break;
default:
renderRawEvents(evs);
}
}
});
})();
Expand Down Expand Up @@ -691,13 +721,21 @@
return "";
};

const collect = (arr) =>
(arr || []).map(msgText).filter(Boolean).join(" | ");
const collectNested = (items) =>
(items || [])
.flatMap((it) => collect(it.data ?? []).split(" | "))
const collect = (arr) => {
if (!Array.isArray(arr)) return "";
return arr.map(msgText).filter(Boolean).join(" | ");
};
const collectNested = (items) => {
if (!Array.isArray(items)) return "";
return items
.flatMap((it) => {
const data = it.data ?? it;
if (!Array.isArray(data)) return [];
return collect(data).split(" | ");
})
.filter(Boolean)
.join(" | ");
};

const level = (e) => {
if (e.event_type.startsWith("Assistant")) return 0;
Expand All @@ -712,6 +750,7 @@
0: "bg-sky-50",
1: "bg-indigo-50",
2: "bg-emerald-50",
2.5: "bg-teal-50",
3: "bg-amber-50",
};

Expand Down Expand Up @@ -884,11 +923,15 @@
.filter((e) => e.event_type === "AssistantRespond")
.map((e) => {
const inp = e.input_data
? e.input_data.map((msg) => ({ content: msg.content }))
: [];
const out = e.output_data
? e.output_data.map((msg) => ({ content: msg.content }))
? e.input_data.data.map((msg) => ({ content: msg.content }))
: [];
const out = Array.isArray(e.output_data)
? e.output_data.flatMap(ev =>
Array.isArray(ev.data)
? ev.data.map(msg => ({ content: msg.content }))
: []
)
: [];
return { input_data: inp, output_data: out };
});
renderHistory(panes.History, hist);
Expand Down
58 changes: 27 additions & 31 deletions grafi_dev/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,26 @@ class ChatReply(BaseModel):
messages: Messages


def _to_messages(msgs_in: List[MsgIn]) -> Messages:
from grafi.common.models.message import Message

return [Message(role=m.role, content=m.content) for m in msgs_in]


def _invoke_context(conv_id: str, req_id: str):
def _to_publish_topic_event(conv_id: str, req_id: str, msgs_in: List[MsgIn]):
from grafi.common.events.topic_events.publish_to_topic_event import (
PublishToTopicEvent,
)
from grafi.common.models.invoke_context import InvokeContext
from grafi.common.models.message import Message

return InvokeContext(
conversation_id=conv_id,
assistant_request_id=req_id,
invoke_id=uuid.uuid4().hex,
return PublishToTopicEvent(
invoke_context=InvokeContext(
conversation_id=conv_id,
assistant_request_id=req_id,
invoke_id=uuid.uuid4().hex,
),
data=[Message(role=m.role, content=m.content) for m in msgs_in],
)


# ---------- conversation helpers ----------------------------------------
def get_conversation_ids():
evs = container.event_store.get_events()
async def get_conversation_ids():
evs = await container.event_store.get_events()
conv_ids = {e.invoke_context.conversation_id for e in evs}
return sorted(
conv_ids,
Expand All @@ -62,8 +63,8 @@ def get_conversation_ids():
)


def get_request_ids(conv_id: str):
evs = container.event_store.get_conversation_events(conv_id)
async def get_request_ids(conv_id: str):
evs = await container.event_store.get_conversation_events(conv_id)
req_ids = {e.invoke_context.assistant_request_id for e in evs}
return sorted(
req_ids,
Expand All @@ -74,25 +75,20 @@ def get_request_ids(conv_id: str):


# ---------- FastAPI factory ---------------------------------------------
def create_app(assistant: Assistant, is_async: bool = True) -> FastAPI:
def create_app(assistant: Assistant, is_sequential: bool = True) -> FastAPI:
api = FastAPI(title="Graphite-Dev API")

@api.post("/chat", response_model=ChatReply)
async def chat(req: ChatRequest):
try:
out: Messages = []
if is_async:

async for messages in assistant.a_invoke(
_invoke_context(req.conversation_id, req.assistant_request_id),
_to_messages(req.messages),
):
out.extend(messages)
else:
out = assistant.invoke(
_invoke_context(req.conversation_id, req.assistant_request_id),
_to_messages(req.messages),
)
async for event in assistant.invoke(
_to_publish_topic_event(
req.conversation_id, req.assistant_request_id, req.messages
),
is_sequential=is_sequential,
):
out.extend(event.data)
logger.info(out)
return ChatReply(messages=out)
except Exception as exc:
Expand All @@ -107,7 +103,7 @@ async def chat(req: ChatRequest):
async def events_convo_dump(conv_id: str):
return [
e.model_dump() # type: ignore
for e in container.event_store.get_conversation_events(conv_id)
for e in await container.event_store.get_conversation_events(conv_id)
]

@api.get("/workflow", response_model=dict)
Expand All @@ -116,11 +112,11 @@ async def workflow():

@api.get("/conversations", response_model=list[str])
async def list_convs():
return get_conversation_ids()
return await get_conversation_ids()

@api.get("/conversations/{conv_id}/requests", response_model=list[str])
async def list_reqs(conv_id: str):
return get_request_ids(conv_id)
return await get_request_ids(conv_id)

ui_dir = Path(__file__).parent / "frontend"
api.mount("/", StaticFiles(directory=ui_dir, html=True), name="ui")
Expand Down
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@ build-backend = "setuptools.build_meta"

[project]
name = "grafi-dev"
version = "0.0.7"
version = "0.0.9"
description = "Run a grafi Assistant locally with a live workflow graph & trace viewer"
authors = [{ name = "Craig Li", email = "craig@binome.dev" }]
readme = "README.md"
requires-python = ">=3.10,<3.13"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.12",
"grafi>=0.0.21",
"grafi>=0.0.31",
"typer>=0.15.3",
"uvicorn>=0.34.2",
]




[project.scripts]
grafi-dev = "grafi_dev.cli:app"

Expand Down
Loading