Add updated get_context to SDKs (#231)

* feat: add optional JWT and webhook secrets to honcho instance creation

* chore: ignore spurious warnings

* feat: add response format if using gpt-5 model family

* feat: add response models to all apis except anthropic

* fix: raise NotImplementedError for response models in AsyncAnthropic client

* chore: address review

* [WIP] representation structure + deriver cleanup

* chore: add tests, cleanup

* feat: [WIP: semi-working] representation object

* fix: alignment

* fix: make observations hashable for dedup

* fix: datetime formatting, observation counting

* fix: switch to int for message id, clean up representation

* feat: remove need for metadata working rep

* chore: cleanup

* fix: use tenacity instead of custom fns

* feat: add representation and card to context if desired

* feat: add semantically relevant observations

* fix: pass all params to streaming, nonblocking streaming

* feat: consolidate document saving, make working representation fetching much smarter

* chore: add 100% test coverage of representation util

* feat: basic dream infra

* feat: dream queue item first pass

* chore: fixes & cleanup from coderabbit

* fix: dreams scheduled when new document count reaches a certain threshold

* feat: wip: timed dreams (not working)

* fix: test

* fix: remove useless pyright ignore

* fix: executing dreams

* feat: dreaming

* feat: [WIP] longmemeval bench

* feat: add USE_PEER_CARD setting, fix longmem test driver

* feat: get full working rep for dialectic in one swoop -- fix representation_from_documents to use the proper timestamp!

* fix: timestamps for real, handle assistant qs in longmem

* fix: remove old client, add batching to longmem

* perf: remove duplicate detection, will move to background task

* feat: track perf metrics on evals

* feat: adjust deriver prompt to use peer_id, add question date to question, clean up deriver

* fix: label metrics by task for better perf trace

* chore: code review

* feat: add efficiency score to longmem bench

* chore: tuning and cleaning up eval

* chore: bring in the big prompts

* feat: add support for vllm client

* feat: perf: bundle db calls in deriver and dialectic, increase max conns in docker db

* feat: [WIP] realtime context object
note: must download custom stainless API for SDK

* feat: add merge-sessions flag to longmemeval, add SUMMARY_ENABLED flag

* fix: COLLECT_METRICS default false

* chore: display start/end message ids, don't include in metrics

* fix: break large messages apart for eval

* fix: only get/create collection when needed

* feat: properly attribute documents with message id ranges and add session name column to documents

* fix: revert move of get_or_create_collection (need for fkey)

* fix: always get collection with peer name even if it's none

* chore: coderabbit

* fix: bug in get context
feat: get context updates in ts sdk

* feat: viz

* chore: update honcho-ai/core, remove WIPs

* fix: consistent ordering, comment nits, removed excess dreamer init

* fix: test int->str

* fix: Add validation and update async python client

* fix: add validation for last_user_message as well

* fix: add deeper validation to getContext in typescript sdk

* fix: let session context take a Message object for lastUserMessage to match python sdk behavior

* fix: use PeerIdSchema

* fix: allow peer object as argument

* fix: lastUserMessage min length 1

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
This commit is contained in:
doria 2025-10-09 15:44:21 -04:00 committed by GitHub
parent f38230fd92
commit a3d98afdfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 365 additions and 52 deletions

View File

@ -8,7 +8,7 @@ authors = [
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
]
dependencies = [
"honcho-core>=1.4.1-alpha.0",
"honcho-core>=1.5.0",
"httpx>=0.28.0, <1",
"pydantic>=2.0.0, <3",
"typing-extensions>=4.12.0; python_version < \"3.12\"",

View File

@ -427,11 +427,23 @@ class AsyncSession(BaseModel):
tokens: int | None = Field(
None, gt=0, description="Maximum number of tokens to include in the context"
),
peer_target: str | None = Field(
None,
description="A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.",
),
last_user_message: str | Message | None = Field(
None,
description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.",
),
peer_perspective: str | None = Field(
None,
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
),
) -> SessionContext:
"""
Get optimized context for this session within a token limit.
Makes an async API call to retrieve a curated list of messages that provides
Makes an API call to retrieve a curated list of messages that provides
optimal context for the conversation while staying within the specified
token limit. Uses tiktoken for token counting, so results should be
compatible with OpenAI models.
@ -440,6 +452,9 @@ class AsyncSession(BaseModel):
summary: Whether to include summary information
tokens: Maximum number of tokens to include in the context. Will default
to Honcho server configuration if not provided.
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
Returns:
A SessionContext object containing the optimized message history and
@ -450,11 +465,32 @@ class AsyncSession(BaseModel):
Token counting is performed using tiktoken. For models using different
tokenizers, you may need to adjust the token limit accordingly.
"""
if peer_target is None and peer_perspective is not None:
raise ValueError(
"You must provide a `peer_target` when `peer_perspective` is provided"
)
if peer_target is None and last_user_message is not None:
raise ValueError(
"You must provide a `peer_target` when `last_user_message` is provided"
)
last_user_message_id = (
last_user_message.id
if isinstance(last_user_message, Message)
else last_user_message
)
context = await self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
tokens=tokens if tokens is not None else omit,
summary=summary,
last_message=last_user_message_id
if last_user_message_id is not None
else omit,
peer_target=peer_target if peer_target is not None else omit,
peer_perspective=peer_perspective if peer_perspective is not None else omit,
)
# Convert the honcho_core summary to our Summary if it exists
@ -469,7 +505,13 @@ class AsyncSession(BaseModel):
)
return SessionContext(
session_id=self.id, messages=context.messages, summary=session_summary
session_id=self.id,
messages=context.messages,
summary=session_summary,
peer_representation=str(context.peer_representation)
if context.peer_representation
else None,
peer_card=context.peer_card,
)
async def get_summaries(self) -> SessionSummaries:

View File

@ -407,6 +407,18 @@ class Session(BaseModel):
tokens: int | None = Field(
None, gt=0, description="Maximum number of tokens to include in the context"
),
peer_target: str | None = Field(
None,
description="A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.",
),
last_user_message: str | Message | None = Field(
None,
description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.",
),
peer_perspective: str | None = Field(
None,
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
),
) -> SessionContext:
"""
Get optimized context for this session within a token limit.
@ -420,6 +432,9 @@ class Session(BaseModel):
summary: Whether to include summary information
tokens: Maximum number of tokens to include in the context. Will default
to Honcho server configuration if not provided.
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
Returns:
A SessionContext object containing the optimized message history and
@ -430,11 +445,32 @@ class Session(BaseModel):
Token counting is performed using tiktoken. For models using different
tokenizers, you may need to adjust the token limit accordingly.
"""
if peer_target is None and peer_perspective is not None:
raise ValueError(
"You must provide a `peer_target` when `peer_perspective` is provided"
)
if peer_target is None and last_user_message is not None:
raise ValueError(
"You must provide a `peer_target` when `last_user_message` is provided"
)
last_user_message_id = (
last_user_message.id
if isinstance(last_user_message, Message)
else last_user_message
)
context = self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
tokens=tokens if tokens is not None else omit,
summary=summary,
last_message=last_user_message_id
if last_user_message_id is not None
else omit,
peer_target=peer_target if peer_target is not None else omit,
peer_perspective=peer_perspective if peer_perspective is not None else omit,
)
# Convert the honcho_core summary to our Summary if it exists
@ -449,7 +485,13 @@ class Session(BaseModel):
)
return SessionContext(
session_id=self.id, messages=context.messages, summary=session_summary
session_id=self.id,
messages=context.messages,
summary=session_summary,
peer_representation=str(context.peer_representation)
if context.peer_representation
else None,
peer_card=context.peer_card,
)
def get_summaries(self) -> SessionSummaries:

View File

@ -58,6 +58,14 @@ class SessionContext(BaseModel):
summary: Summary | None = Field(
None, description="Summary of the session history prior to the message cutoff"
)
peer_representation: str | None = Field(
None,
description="The peer representation, if context is requested from a specific perspective",
)
peer_card: list[str] | None = Field(
None,
description="The peer card, if context is requested from a specific perspective",
)
@validate_call
def __init__(
@ -72,6 +80,14 @@ class SessionContext(BaseModel):
None,
description="Summary of the session history prior to the message cutoff",
),
peer_representation: str | None = Field(
None,
description="The peer representation, if context is requested from a specific perspective",
),
peer_card: list[str] | None = Field(
None,
description="The peer card, if context is requested from a specific perspective",
),
) -> None:
"""
Initialize a new SessionContext.
@ -84,6 +100,8 @@ class SessionContext(BaseModel):
session_id=session_id,
messages=messages,
summary=summary,
peer_representation=peer_representation,
peer_card=peer_card,
)
def to_openai(
@ -117,14 +135,30 @@ class SessionContext(BaseModel):
}
for message in self.messages
]
system_messages: list[dict[str, str]] = []
if self.peer_representation:
peer_representation_message = {
"role": "system",
"content": f"<peer_representation>{self.peer_representation}</peer_representation>",
}
system_messages.append(peer_representation_message)
if self.peer_card:
peer_card_message = {
"role": "system",
"content": f"<peer_card>{self.peer_card}</peer_card>",
}
system_messages.append(peer_card_message)
if self.summary:
summary_message = {
"role": "system",
"content": f"<summary>{self.summary.content}</summary>",
}
return [summary_message, *messages]
return messages
system_messages.append(summary_message)
return system_messages + messages
def to_anthropic(
self,
@ -164,14 +198,30 @@ class SessionContext(BaseModel):
}
for message in self.messages
]
system_messages: list[dict[str, str]] = []
if self.peer_representation:
peer_representation_message = {
"role": "user",
"content": f"<peer_representation>{self.peer_representation}</peer_representation>",
}
system_messages.append(peer_representation_message)
if self.peer_card:
peer_card_message = {
"role": "user",
"content": f"<peer_card>{self.peer_card}</peer_card>",
}
system_messages.append(peer_card_message)
if self.summary:
summary_message = {
"role": "user",
"content": f"<summary>{self.summary.content}</summary>",
}
return [summary_message, *messages]
return messages
system_messages.append(summary_message)
return system_messages + messages
def __len__(self) -> int:
"""

View File

@ -23,7 +23,7 @@ function createTestMessage(id: string, content: string, peer_id: string, additio
function createTestSummary(content: string): Summary {
return new Summary({
content,
message_id: 1,
message_id: "1",
summary_type: 'short',
created_at: new Date().toISOString(),
token_count: content.length

View File

@ -4,7 +4,7 @@
"": {
"name": "@honcho-ai/sdk",
"dependencies": {
"@honcho-ai/core": "^1.4.1-alpha.0",
"@honcho-ai/core": "^1.5.0",
"@types/node": "^24.0.1",
"zod": "4.0.0",
},
@ -108,7 +108,7 @@
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
"@honcho-ai/core": ["@honcho-ai/core@1.4.1-alpha.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-cRp3ug8cBe9E7NZDgCXkI0EHIgm8x5Jq/nQB0xA5629GuniS99yq6M786I//FjOZhB2YEZoOxd4Zkg412uk5Yg=="],
"@honcho-ai/core": ["@honcho-ai/core@1.5.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-kjYhCO0S9Ll3DfVpK8MetjY1PlD9FG6QGtudlMnSwfTWGzXFjL/OafpoE3SZ3c9BVY48JIbW9IttFUZFekv/xg=="],
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],

View File

@ -20,7 +20,7 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@honcho-ai/core": "^1.4.1-alpha.0",
"@honcho-ai/core": "^1.5.0",
"@types/node": "^24.0.1",
"zod": "4.0.0"
},

View File

@ -451,10 +451,21 @@ export class Session {
* compatible with OpenAI models. The context optimization balances
* recency and relevance to provide the best conversational context.
*
* @param summary - Whether to include summary information in the context.
* When true, includes session summary if available. Defaults to true
* @param tokens - Maximum number of tokens to include in the context. If not provided,
* uses the server's default configuration
* @param options - Configuration options for context retrieval
* @param options.summary - Whether to include summary information in the context.
* When true, includes session summary if available. Defaults to true
* @param options.tokens - Maximum number of tokens to include in the context. If not provided,
* uses the server's default configuration
* @param options.peerTarget - The target of the perspective. If given without `peerPerspective`,
* will get the Honcho-level representation and peer card for this peer.
* If given with `peerPerspective`, will get the representation and card
* for this peer from the perspective of that peer.
* @param options.lastUserMessage - The most recent message, used to fetch semantically relevant
* observations and returned as part of the context object.
* Can be either a message ID string or a Message object.
* @param options.peerPerspective - A peer to get context for. If given, response will attempt to
* include representation and card from the perspective of that peer.
* Must be provided with `peerTarget`.
* @returns Promise resolving to a SessionContext object containing the optimized
* message history and summary (if available) that maximizes conversational
* context while respecting the token limit
@ -462,25 +473,103 @@ export class Session {
* @note Token counting is performed using tiktoken. For models using different
* tokenizers, you may need to adjust the token limit accordingly.
*/
async getContext(
summary?: boolean,
tokens?: number,
peerTarget?: string | Peer,
lastUserMessage?: string | Message,
peerPerspective?: string | Peer
): Promise<SessionContext>
async getContext(options?: {
summary?: boolean
tokens?: number
}): Promise<SessionContext> {
peerTarget?: string | Peer
lastUserMessage?: string | Message
peerPerspective?: string | Peer
}): Promise<SessionContext>
async getContext(
summaryOrOptions?:
| boolean
| {
summary?: boolean
tokens?: number
peerTarget?: string | Peer
lastUserMessage?: string | Message
peerPerspective?: string | Peer
},
tokens?: number,
peerTarget?: string | Peer,
lastUserMessage?: string | Message,
peerPerspective?: string | Peer
): Promise<SessionContext> {
// Normalize positional arguments into options object
let options: {
summary?: boolean
tokens?: number
peerTarget?: string
lastUserMessage?: string
peerPerspective?: string
}
if (
typeof summaryOrOptions === 'boolean' ||
(summaryOrOptions === undefined && arguments.length > 1)
) {
// Positional arguments pattern
options = {
summary: summaryOrOptions as boolean | undefined,
tokens,
peerTarget: typeof peerTarget === 'object' ? peerTarget.id : peerTarget,
lastUserMessage:
typeof lastUserMessage === 'string'
? lastUserMessage
: lastUserMessage?.id,
peerPerspective:
typeof peerPerspective === 'object'
? peerPerspective.id
: peerPerspective,
}
} else {
// Options object pattern
options = (summaryOrOptions as typeof options) || {}
}
const contextParams = ContextParamsSchema.parse({
summary: options?.summary,
tokens: options?.tokens,
summary: options.summary,
tokens: options.tokens,
peerTarget: options.peerTarget,
lastUserMessage: options.lastUserMessage,
peerPerspective: options.peerPerspective,
})
// Extract message ID if lastUserMessage is a Message object
const lastMessageId =
typeof contextParams.lastUserMessage === 'string'
? contextParams.lastUserMessage
: contextParams.lastUserMessage?.id
const context = await this._client.workspaces.sessions.getContext(
this.workspaceId,
this.id,
{
tokens: contextParams.tokens,
summary: contextParams.summary,
last_message: lastMessageId,
peer_target: contextParams.peerTarget,
peer_perspective: contextParams.peerPerspective,
}
)
// Convert the summary response to Summary object if present
const summary = context.summary ? new Summary(context.summary) : null
return new SessionContext(this.id, context.messages, summary)
return new SessionContext(
this.id,
context.messages,
summary,
context.peer_representation
? JSON.stringify(context.peer_representation)
: null,
context.peer_card ?? null
)
}
/**

View File

@ -102,21 +102,37 @@ export class SessionContext {
*/
readonly summary: Summary | null
/**
* The peer representation, if context is requested from a specific perspective.
*/
readonly peerRepresentation: string | null
/**
* The peer card, if context is requested from a specific perspective.
*/
readonly peerCard: string[] | null
/**
* Initialize a new SessionContext.
*
* @param sessionId ID of the session this context belongs to
* @param messages List of Message objects to include in the context
* @param summary Summary of the session history prior to the message cutoff
* @param peerRepresentation The peer representation, if context is requested from a specific perspective
* @param peerCard The peer card, if context is requested from a specific perspective
*/
constructor(
sessionId: string,
messages: Message[],
summary: Summary | null = null
summary: Summary | null = null,
peerRepresentation: string | null = null,
peerCard: string[] | null = null
) {
this.sessionId = sessionId
this.messages = messages
this.summary = summary
this.peerRepresentation = peerRepresentation
this.peerCard = peerCard
}
/**
@ -136,18 +152,36 @@ export class SessionContext {
assistant: string | Peer
): Array<{ role: string; content: string; name?: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
const summaryMessage = this.summary
? {
role: 'system',
content: `<summary>${this.summary.content}</summary>`,
}
: null
const messages = this.messages.map((message) => ({
role: message.peer_id === assistantId ? 'assistant' : 'user',
name: message.peer_id,
content: message.content,
}))
return summaryMessage ? [summaryMessage, ...messages] : messages
const systemMessages: Array<{ role: string; content: string }> = []
if (this.peerRepresentation) {
systemMessages.push({
role: 'system',
content: `<peer_representation>${this.peerRepresentation}</peer_representation>`,
})
}
if (this.peerCard) {
systemMessages.push({
role: 'system',
content: `<peer_card>${this.peerCard}</peer_card>`,
})
}
if (this.summary) {
systemMessages.push({
role: 'system',
content: `<summary>${this.summary.content}</summary>`,
})
}
return [...systemMessages, ...messages]
}
/**
@ -170,12 +204,6 @@ export class SessionContext {
assistant: string | Peer
): Array<{ role: string; content: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
const summaryMessage = this.summary
? {
role: 'user',
content: `<summary>${this.summary.content}</summary>`,
}
: null
const messages = this.messages.map((message) =>
message.peer_id === assistantId
? {
@ -187,7 +215,31 @@ export class SessionContext {
content: `${message.peer_id}: ${message.content}`,
}
)
return summaryMessage ? [summaryMessage, ...messages] : messages
const systemMessages: Array<{ role: string; content: string }> = []
if (this.peerRepresentation) {
systemMessages.push({
role: 'user',
content: `<peer_representation>${this.peerRepresentation}</peer_representation>`,
})
}
if (this.peerCard) {
systemMessages.push({
role: 'user',
content: `<peer_card>${this.peerCard}</peer_card>`,
})
}
if (this.summary) {
systemMessages.push({
role: 'user',
content: `<summary>${this.summary.content}</summary>`,
})
}
return [...systemMessages, ...messages]
}
/**

View File

@ -1,3 +1,4 @@
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
import { z } from 'zod'
/**
@ -123,16 +124,56 @@ export const ChatQuerySchema = z.object({
sessionId: z.string().optional(),
})
/**
* Schema for validating Message objects from the core SDK.
*/
const MessageSchema: z.ZodType<Message> = z.object({
id: z.string(),
content: z.string(),
created_at: z.string(),
peer_id: z.string(),
session_id: z.string(),
token_count: z.number(),
workspace_id: z.string(),
metadata: z.record(z.string(), z.unknown()).optional(),
}) as z.ZodType<Message>
/**
* Schema for context retrieval parameters.
*/
export const ContextParamsSchema = z.object({
summary: z.boolean().optional(),
tokens: z
.number()
.positive('Token limit must be a positive number')
.optional(),
})
export const ContextParamsSchema = z
.object({
summary: z.boolean().optional(),
tokens: z
.number()
.positive('Token limit must be a positive number')
.optional(),
lastUserMessage: z
.union([
z.string().min(1, 'Last user message must be a non-empty string'),
MessageSchema,
])
.optional(),
peerTarget: PeerIdSchema.optional(),
peerPerspective: PeerIdSchema.optional(),
})
.superRefine((data, ctx) => {
if (data.lastUserMessage && !data.peerTarget) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'peerTarget is required when lastUserMessage is provided',
path: ['lastUserMessage'],
})
}
if (data.peerPerspective && !data.peerTarget) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'peerTarget is required when peerPerspective is provided',
path: ['peerPerspective'],
})
}
})
/**
* Schema for deriver status options.

View File

@ -436,11 +436,8 @@ try:
print_settings(value, full_key, max_depth, current_depth + 1)
else:
# Mask sensitive information
if isinstance(value, str) and any(sensitive in value.lower() for sensitive in ['password', 'secret', 'key', 'token']):
if 'testpwd' in value:
masked_value = value.replace('testpwd', '***')
else:
masked_value = '***'
if isinstance(full_key, str) and any(sensitive in full_key.lower() for sensitive in ['password', 'secret', 'key', 'uri']):
masked_value = '*' * len(value) if value else 'None'
else:
masked_value = value
print(f" {{key}}: {{masked_value}}")

10
uv.lock
View File

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13'",
@ -780,7 +780,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "honcho-core", specifier = ">=1.4.1a0" },
{ name = "honcho-core", specifier = ">=1.5.0" },
{ name = "httpx", specifier = ">=0.28.0,<1" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" },
@ -791,7 +791,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
[[package]]
name = "honcho-core"
version = "1.4.1a0"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -801,9 +801,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/fbf3ca017ffcdaef92e26fe84b77268c1c0b26f95e282d8684f50932d81a/honcho_core-1.4.1a0.tar.gz", hash = "sha256:e0023f46c65500d727e3fb1275781ae1e0b162c226a01352afd45c421c5f350b", size = 130826, upload-time = "2025-10-01T03:23:27.143Z" }
sdist = { url = "https://files.pythonhosted.org/packages/75/cd/05d2a21afd037673637e390de411cd66ca56b07e524cd7ead65c655a2b49/honcho_core-1.5.0.tar.gz", hash = "sha256:4876195dad16db437117d40a1d5e34ff88974e8eca6d093f3653b4ac2bda3c6d", size = 132236, upload-time = "2025-10-08T18:30:15.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/5f/2aa746b7fd92690ef782dcacc56a5c47a19a999b9729597c04c7cc509e52/honcho_core-1.4.1a0-py3-none-any.whl", hash = "sha256:8b4c86e3ac3964ff56fe6a449b9fa23be49530609c34bdf35c9ecde7cd871bba", size = 121704, upload-time = "2025-10-01T03:23:25.66Z" },
{ url = "https://files.pythonhosted.org/packages/ef/4e/9ef193bba00c521be1152791d2ba37a98bdb58ece8f8ce7863b710a4f511/honcho_core-1.5.0-py3-none-any.whl", hash = "sha256:01db345371d6e80230b202c797a73f9f086dc244936522dbc39efb05f4cee306", size = 123210, upload-time = "2025-10-08T18:30:14.277Z" },
]
[[package]]