Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Not finished][Do not review] #6823

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 20 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
26 changes: 26 additions & 0 deletions frontend/src/components/shared/task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function TaskForm({ ref }: TaskFormProps) {
});
const [inputIsFocused, setInputIsFocused] = React.useState(false);
const { mutate: createConversation, isPending } = useCreateConversation();
const [estimateCost, setEstimateCost] = React.useState(false);

const onRefreshSuggestion = () => {
const suggestions = SUGGESTIONS["non-repo"];
Expand Down Expand Up @@ -103,6 +104,31 @@ export function TaskForm({ ref }: TaskFormProps) {
/>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-400">Estimate cost</span>
<button
type="button"
role="switch"
aria-checked={estimateCost}
onClick={() => setEstimateCost(!estimateCost)}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full",
"transition-colors duration-200 ease-in-out",
estimateCost ? "bg-green-500" : "bg-neutral-600",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
)}
>
<span className="sr-only">Enable cost estimation</span>
<span
className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white",
"transition duration-200 ease-in-out",
"shadow-lg",
estimateCost ? "translate-x-6" : "translate-x-1"
)}
/>
</button>
</div>
</form>
<UploadImageInput
onUpload={async (uploadedFiles) => {
Expand Down
22 changes: 19 additions & 3 deletions frontend/src/services/observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,26 @@ import {
} from "#/state/chat-slice";

export function handleObservationMessage(message: ObservationMessage) {
console.log("Processing observation message:", message);

// Handle llm_metrics and usage together
if (message.llm_metrics || message.tool_call_metadata?.model_response?.usage) {
const usage = message.tool_call_metadata?.model_response?.usage;
const metrics = message.llm_metrics;
console.log("Processing metrics:", metrics);
console.log("Processing usage:", usage);

alert([
'LLM Information',
metrics ? `Accumulated Cost: $${metrics.accumulated_cost.toFixed(4)}` : '',
usage ? `Prompt Tokens: ${usage.prompt_tokens}` : '',
usage ? `Completion Tokens: ${usage.completion_tokens}` : '',
usage ? `Total Tokens: ${usage.total_tokens}` : ''
].filter(line => line !== '').join('\n')); // Filter out empty lines
}

switch (message.observation) {
case ObservationType.RUN: {
case ObservationType.RUN:
if (message.extras.hidden) break;
let { content } = message;

Expand All @@ -24,9 +42,7 @@ export function handleObservationMessage(message: ObservationMessage) {

store.dispatch(appendOutput(content));
break;
}
case ObservationType.RUN_IPYTHON:
// FIXME: render this as markdown
store.dispatch(appendJupyterOutput(message.content));
break;
case ObservationType.BROWSE:
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/types/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ export interface ObservationMessage {

// The timestamp of the message
timestamp: string;

// 添加 tool_call_metadata 字段
tool_call_metadata?: {
model_response?: {
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
};
};

llm_metrics?: {
accumulated_cost: number;
response_latencies: Array<{
latency: number;
timestamp: string;
}>;
};
}

export interface StatusMessage {
Expand Down
8 changes: 8 additions & 0 deletions openhands/agenthub/codeact_agent/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
FunctionCallNotExistsError,
FunctionCallValidationError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
AgentDelegateAction,
Expand Down Expand Up @@ -590,6 +591,13 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
total_calls_in_response=len(assistant_msg.tool_calls),
)
actions.append(action)

# Add logging for each created action
for action in actions:
accumulated_cost = action.llm_metrics.accumulated_cost if action.llm_metrics else None
logger.info(f"Action created - Accumulated Cost: {accumulated_cost}")
logger.info(f"Action type: {type(action)}")

else:
actions.append(
MessageAction(content=assistant_msg.content, wait_for_response=True)
Expand Down
15 changes: 15 additions & 0 deletions openhands/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ def update_state_before_step(self):
async def update_state_after_step(self):
# update metrics especially for cost. Use deepcopy to avoid it being modified by agent._reset()
self.state.local_metrics = copy.deepcopy(self.agent.llm.metrics)

# Add logging for metrics update
accumulated_cost = self.state.local_metrics.accumulated_cost if self.state.local_metrics else None
logger.info(f"After update_state_after_step - State Local Metrics Accumulated Cost: {accumulated_cost}")

async def _react_to_exception(
self,
Expand Down Expand Up @@ -379,6 +383,17 @@ async def _handle_observation(self, observation: Observation) -> None:
if observation.llm_metrics is not None:
self.agent.llm.metrics.merge(observation.llm_metrics)

logger.info("Mark the position of _handle_observation")
# Log state metrics before assignment
if self.state and self.state.local_metrics:
logger.info(f"State local_metrics before assignment - accumulated cost: {self.state.local_metrics.accumulated_cost}")

# Add local metrics to observation
if self.state and self.state.local_metrics:
observation.llm_metrics = copy.deepcopy(self.state.local_metrics)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if we added it, instead of observation, to the action? Somewhere

  • after we got it from the agent at line 689
action = self.agent.step(self.state)
  • and just before it's added to the event stream, like before this line 751
self.event_stream.add_event(action, action._source)  # type: ignore [attr-defined]

Then it would be saved in the stream, so the server would be able to read it in its dict it sends to frontend. 🤔

I think we have some difficult problem atm with sending extra bits of information to the frontend, I'm sorry about that. This idea might be one way to do it, and it should work for our purpose.

Copy link
Author

Choose a reason for hiding this comment

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

🤗I tried to hard-code llm_metrics in session.py before sending the "oh_event" response in send, and it successfully appeared on the frontend. Then, I went back to check where preparing the related event and found that adding llm_metrics in actions with EventSource.AGENT allows the content to be successfully pushed into the event stream.

As a result, all subscribers, including EventStreamSubscriber.SERVER in session.py, can fetch it. Ultimately, it gets wrapped in the "oh_event" and sent to the frontend.

I guess I understand the full logic now.Including your advice's backend reason

🤔But initially when I tried to add the code following your advice to test, it doesn't work. Let me check again and continue the debugging process. Thanks for your kindness. I have idea now I think!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think eventstream.add_event saves the event, with everything in it, including _llm_metrics if we set it on the event. So the event gets transmitted to subscribers, including SERVER, with its contents.

# Log observation metrics after assignment
logger.info(f"Observation metrics after assignment - accumulated cost: {observation.llm_metrics.accumulated_cost}")

if self._pending_action and self._pending_action.id == observation.cause:
if self.state.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
return
Expand Down
7 changes: 7 additions & 0 deletions openhands/events/serialization/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from openhands.events.serialization.observation import observation_from_dict
from openhands.events.serialization.utils import remove_fields
from openhands.events.tool import ToolCallMetadata
from openhands.core.logger import openhands_logger as logger

# TODO: move `content` into `extras`
TOP_KEYS = [
Expand All @@ -20,6 +21,7 @@
'action',
'observation',
'tool_call_metadata',
'llm_metrics'
]
UNDERSCORE_KEYS = ['id', 'timestamp', 'source', 'cause', 'tool_call_metadata']

Expand Down Expand Up @@ -80,7 +82,12 @@ def event_to_dict(event: 'Event') -> dict:
if key == 'source' and 'source' in d:
d['source'] = d['source'].value
if key == 'tool_call_metadata' and 'tool_call_metadata' in d:
logger.info(f"Mark the position of tool_call_metadata processing in event_to_dict")
d['tool_call_metadata'] = d['tool_call_metadata'].model_dump()
if key == 'llm_metrics' and 'llm_metrics' in d:
logger.info(f"Before llm_metrics processing - metrics in d: {d['llm_metrics']}")
d['llm_metrics'] = d['llm_metrics'].get()
logger.info(f"After llm_metrics processing - metrics result: {d['llm_metrics']}")
props.pop(key, None)
if 'security_risk' in props and props['security_risk'] is None:
props.pop('security_risk')
Expand Down
8 changes: 8 additions & 0 deletions openhands/events/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ async def __aiter__(self):

# Create an async generator that yields events
for event in self.event_stream.get_events(*self.args, **self.kwargs):
# Log event type and metrics if present
logger.info(f"AsyncEventStreamWrapper - Event type: {type(event)}")
if hasattr(event, 'llm_metrics') and event.llm_metrics:
logger.info(f"AsyncEventStreamWrapper - Accumulated Cost: {event.llm_metrics.accumulated_cost}")
# Run the blocking get_events() in a thread pool
yield await loop.run_in_executor(None, lambda e=event: e) # type: ignore

Expand Down Expand Up @@ -262,6 +266,10 @@ def unsubscribe(self, subscriber_id: EventStreamSubscriber, callback_id: str):
self._clean_up_subscriber(subscriber_id, callback_id)

def add_event(self, event: Event, source: EventSource):
# Add logs for accumulated cost debugging.
accumulated_cost = event.llm_metrics.accumulated_cost if event.llm_metrics else None
logger.info(f"Adding event to stream - Accumulated Cost: {accumulated_cost}")

if hasattr(event, '_id') and event.id is not None:
raise ValueError(
f'Event already has an ID:{event.id}. It was probably added back to the EventStream from inside a handler, triggering a loop.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.request import send_request
from openhands.utils.http_session import HttpSession
from openhands.core.logger import openhands_logger as logger


class ActionExecutionClient(Runtime):
Expand Down Expand Up @@ -217,6 +218,11 @@ def get_vscode_token(self) -> str:
return ''

def send_action_for_execution(self, action: Action) -> Observation:
# Log: check if incoming action has metrics
logger.info(f"Action type before execution: {type(action)}")
if hasattr(action, 'llm_metrics') and action.llm_metrics:
logger.info(f"Action llm_metrics before execution - accumulated cost: {action.llm_metrics.accumulated_cost}")

if (
isinstance(action, FileEditAction)
and action.impl_source == FileEditSource.LLM_BASED_EDIT
Expand Down
7 changes: 7 additions & 0 deletions openhands/server/session/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,18 @@ async def _on_event(self, event: Event):
Args:
event: The agent event (Observation or Action).
"""
# Add logs: Record received events and metrics
logger.info(f"Session received event type: {type(event)}")
if hasattr(event, 'llm_metrics') and event.llm_metrics:
logger.info(f"Event accumulated cost: {event.llm_metrics.accumulated_cost}")

if isinstance(event, NullAction):
return
if isinstance(event, NullObservation):
return
if event.source == EventSource.AGENT:
# Add logs:Record agent->metrics
logger.info(f"Processing AGENT event accumulated cost: {event.llm_metrics.accumulated_cost if hasattr(event, 'llm_metrics') and event.llm_metrics else None}")
await self.send(event_to_dict(event))
elif event.source == EventSource.USER:
await self.send(event_to_dict(event))
Expand Down