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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta

### Changed

- **Skills (finance/uk_companies_house_handler):** Completed Phase v2a upgrade (#220). Added `context` parameter to carry forward session state (`company_number`, `company_name`, `officer_filter`, etc.). Added `partial` envelope status and optional `pipeline` state to support paused multi-step pipelines (noted as v2b prep in instructions). Updated `_get_officers` to fallback to `company_name` from context and added tests for fallback logic. Updated `examples/gemini_uk_companies_house_handler.py` into a fully interactive chat loop.
- **Documentation:** CONTRIBUTING and contributor workflow checklists — update `card.json` and output fixtures together when changing `execute()` output (#199).

## [0.4.5] - 2026-07-16
Expand Down
29 changes: 26 additions & 3 deletions docs/skills/uk_companies_house_handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
**Recommended install:** `pip install "skillware[finance_uk_companies_house_handler]"`. See [Install extras](../usage/install_extras.md).
[Skill Library](README.md) · [Testing](../TESTING.md)

A deterministic UK Companies House API handler for agents. Provides structured operations for company search, profile lookup, officer and PSC listing, filing history, and intent-to-operation mapping with UK corporate terminology translation. Returns status-based responses (`ready`, `needs_input`, `error`) with disambiguation support.
A deterministic UK Companies House API handler for agents. Provides structured operations for company search, profile lookup, officer and PSC listing, filing history, and intent-to-operation mapping with UK corporate terminology translation. Returns status-based responses (`ready`, `partial`, `needs_input`, `error`) with disambiguation support.

## Capabilities

Expand All @@ -16,6 +16,7 @@ A deterministic UK Companies House API handler for agents. Provides structured o
- **Persons with Significant Control (PSC)**: List beneficial owners with natures of control, equivalent to the US concept of "beneficial owner" or "shareholder".
- **Filing History**: List filings (accounts, confirmation statements, incorporations) with optional category filtering and document metadata links.
- **Intent Mapping**: Translate common user intent keywords (CEO, owner, shareholder) to the correct UK Companies House actions and build suggested action pipelines.
- **State Tracking (Context)**: Automatically carries forward session state (like `company_number`, `company_name`, and active filters) between sequential tool calls to seamlessly link multi-step operations.

## Internal Architecture

Expand All @@ -31,7 +32,8 @@ The system prompt teaches the AI to:
### 2. The Body (`skill.py`)
A single `execute()` entry point dispatches to six action handlers:
- **HTTP layer**: Authenticated requests using API key as HTTP Basic username.
- **Status envelope**: Every response includes `status` (ready/needs_input/error), `fetched_at` (UTC ISO), and `source`.
- **Status envelope**: Every response includes `status` (ready/partial/needs_input/error), `fetched_at` (UTC ISO), and `source`.
- **State propagation**: Extracts and updates a strict 5-key `context` schema in every response, falling back to these values if omitted in subsequent requests.
- **Error handling**: Catches HTTP errors (404, 429, 500), timeouts, and connection failures.

### 3. The Knowledge (`data/`)
Expand Down Expand Up @@ -203,6 +205,13 @@ Prompt mode via `SkillLoader.to_ollama_prompt(bundle)`; match `"tool": "finance/
"company_status": "dissolved"
}
],
"context": {
"company_number": null,
"company_name": null,
"last_action": "resolve_company",
"officer_filter": null,
"selected_transaction_id": null
},
"agent_hint": "Ask the user which company they mean before calling get_officers.",
"next_actions": ["get_company_profile", "get_officers"],
"fetched_at": "2026-07-05T00:00:00+00:00"
Expand All @@ -215,7 +224,14 @@ Prompt mode via `SkillLoader.to_ollama_prompt(bundle)`; match `"tool": "finance/
{
"action": "get_officers",
"company_number": "00102498",
"active_only": true
"active_only": true,
"context": {
"company_number": "00102498",
"company_name": "BP P.L.C.",
"last_action": "resolve_company",
"officer_filter": null,
"selected_transaction_id": null
}
}
```

Expand All @@ -225,6 +241,13 @@ Prompt mode via `SkillLoader.to_ollama_prompt(bundle)`; match `"tool": "finance/
{
"status": "ready",
"company_number": "00102498",
"context": {
"company_number": "00102498",
"company_name": "BP P.L.C.",
"last_action": "get_officers",
"officer_filter": null,
"selected_transaction_id": null
},
"officers": [
{
"name": "SMITH, John",
Expand Down
101 changes: 56 additions & 45 deletions examples/gemini_uk_companies_house_handler.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
Non-interactive Gemini agent loop for finance/uk_companies_house_handler.
Interactive Gemini agent loop for finance/uk_companies_house_handler.

Demonstrates the full "Who is the CEO of BP?" flow:
Demonstrates an interactive flow to query UK companies:
1. map_intent -- translate intent keywords to an action pipeline
2. resolve_company -- search by name, receive ranked candidates
3. Agent disambiguates (scripted: picks BP P.L.C.)
3. Agent disambiguates (asks user to pick if needed)
4. get_officers -- list directors for the resolved company number

Environment (live mode):
Expand Down Expand Up @@ -44,59 +44,70 @@ def main() -> None:

system_instruction = bundle["instructions"]

user_query = "Who is the CEO of BP?"
print(f"User: {user_query}\n")

response = client.models.generate_content(
print("\n" + "=" * 60)
print("UK Companies House Gemini Agent")
print("=" * 60)
print("This agent can look up UK companies, officers, PSCs, and filings.")
print("Try asking:")
print(" - 'Who is the CEO of BP?'")
print(" - 'Show me the filing history for Tesco'")
print(" - 'Who owns Monzo?'")
print("\nType 'exit' or 'quit' to stop.")
print("=" * 60)

chat = client.chats.create(
model="gemini-2.5-flash",
contents=user_query,
config=types.GenerateContentConfig(
tools=[tool],
system_instruction=system_instruction,
),
)

while response.candidates and response.candidates[0].content.parts:
part = response.candidates[0].content.parts[0]
if not part.function_call:
while True:
try:
user_query = input("\nUser: ").strip()
except EOFError:
break

fn_name = part.function_call.name
fn_args = dict(part.function_call.args)
print("--- Tool Call ---")
print(f"Function: {fn_name}")
print(f"Arguments: {json.dumps(fn_args, indent=2)}")

expected_tool_name = SkillLoader._sanitize_gemini_tool_name(
bundle["manifest"]["name"]
)
if fn_name != expected_tool_name:
print(f"Unknown tool: {fn_name}")
if not user_query:
continue

if user_query.lower() in ("exit", "quit"):
break

api_result = handle_tool_call(skill, fn_args)
print("\n--- Skill Result ---")
print(json.dumps(api_result, indent=2))

response = client.models.generate_content(
model="gemini-2.5-flash",
contents=[
"Use this tool result to answer the original request.",
{
"function_response": {
"name": fn_name,
"response": {"result": api_result},
}
},
],
config=types.GenerateContentConfig(
tools=[tool],
system_instruction=system_instruction,
),
)

print("\nAgent Final Response:")
print(response.text)
response = chat.send_message(user_query)

while response.function_calls:
# We assume one tool call at a time for simplicity in this example
tool_call = response.function_calls[0]
fn_name = tool_call.name
fn_args = dict(tool_call.args)

print("--- Tool Call ---")
print(f"Function: {fn_name}")
print(f"Arguments: {json.dumps(fn_args, indent=2)}")

expected_tool_name = SkillLoader._sanitize_gemini_tool_name(
bundle["manifest"]["name"]
)
if fn_name != expected_tool_name:
print(f"Unknown tool: {fn_name}")
api_result = {"error": f"Unknown tool: {fn_name}"}
else:
api_result = handle_tool_call(skill, fn_args)

print("\n--- Skill Result ---")
print(json.dumps(api_result, indent=2))

# Send the tool result back to the chat
response = chat.send_message(
types.Part.from_function_response(
name=fn_name, response={"result": api_result}
)
)

if response.text:
print(f"\nAgent: {response.text}")


if __name__ == "__main__":
Expand Down
22 changes: 15 additions & 7 deletions examples/uk_companies_house_handler_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,50 +141,58 @@ def run_scripted_flow(skill: Any) -> None:
}
)
print(json.dumps(intent_result, indent=2))
context = intent_result.get("context", {})

# Step 1: Resolve company
resolve_result = skill.execute(
{"action": "resolve_company", "query": "BP", "limit": 5}
{"action": "resolve_company", "query": "BP", "limit": 5, "context": context}
)
context = resolve_result.get("context", context)
print(json.dumps(resolve_result, indent=2))

# Pick the first candidate
if resolve_result["status"] == "needs_input":
company_number = resolve_result["candidates"][0]["company_number"]
company_name = resolve_result["candidates"][0]["title"]
print(f"\nUser selects: {company_name} ({company_number})\n")
context["company_number"] = company_number
context["company_name"] = company_name
elif resolve_result["status"] == "ready":
company_number = resolve_result["company_number"]
company_name = resolve_result.get("company_name", "")
print(f"\nSingle match: {company_name} ({company_number})\n")
context["company_number"] = company_number
context["company_name"] = company_name
else:
print(f"\nError: {resolve_result.get('message', 'unknown')}")
return

# Step 2: Get company profile
profile_result = skill.execute(
{"action": "get_company_profile", "company_number": company_number}
{"action": "get_company_profile", "context": context}
)
context = profile_result.get("context", context)
print(json.dumps(profile_result, indent=2))

# Step 3: Get officers (active only)
officers_result = skill.execute(
{
"action": "get_officers",
"company_number": company_number,
"active_only": True,
"context": context,
}
)
context = officers_result.get("context", context)
print(json.dumps(officers_result, indent=2))

# Step 4: Get PSCs
psc_result = skill.execute({"action": "get_pscs", "company_number": company_number})
psc_result = skill.execute({"action": "get_pscs", "context": context})
context = psc_result.get("context", context)
print(json.dumps(psc_result, indent=2))

# Step 5: Get filing history
filing_result = skill.execute(
{"action": "get_filing_history", "company_number": company_number}
)
filing_result = skill.execute({"action": "get_filing_history", "context": context})
context = filing_result.get("context", context)
print(json.dumps(filing_result, indent=2))

print("\n=== flow complete ===")
Expand Down
4 changes: 3 additions & 1 deletion skills/finance/uk_companies_house_handler/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,21 @@ Always inform the user about these terminology differences when presenting resul
3. Follow the actions exactly as ordered in the `suggested_pipeline`.
4. If a step (like `resolve_company`) returns a status of `needs_input`, present the candidates to the user and wait for their choice before proceeding.
5. Once you have a confirmed `company_number`, continue with the remaining specific actions (`get_officers`, `get_pscs`, etc.) in your pipeline.
6. **State Tracking (Context)**: Every response from the skill includes a `context` object. You MUST pass this exact `context` object back as an argument to subsequent tool calls in the same session. This allows the skill to remember the active `company_number` and `company_name` without you having to manually extract and pass them every time.

## Understanding responses

Every response includes a `status` field:

- **`ready`**: The data was fetched successfully. Present it to the user.
- **`partial`**: (Note: v2b prep, not currently used) The operation succeeded, but the data returned is incomplete (e.g., due to pagination or a missing sub-resource). Inform the user that there is more information available or synthesize the available data.
- **`needs_input`**: Multiple matches or missing information. Present the `candidates` to the user and ask for clarification. Use the `agent_hint` for guidance.
- **`error`**: Something went wrong. Check `error_code` and `message`. Common errors:
- `not_found`: Company number does not exist.
- `rate_limited`: Too many requests; wait and retry.
- `missing_company_number`: You need to resolve a company first.

Every response includes `fetched_at` (UTC timestamp) and `source` — always cite these when presenting data.
Every response includes `fetched_at` (UTC timestamp), `source`, and a `context` block — always cite the timestamp and source when presenting data. Responses may also include an optional `pipeline` object (Note: v2b prep, not currently used; e.g. `{"completed_steps": 1, "total_steps": 2}`) representing the partial pipeline state if a multi-step operation is paused.

## Limitations

Expand Down
10 changes: 8 additions & 2 deletions skills/finance/uk_companies_house_handler/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: finance/uk_companies_house_handler
version: 1.0.0
version: 1.1.0
description: |
Deterministic UK Companies House API handler for agents. Provides structured
operations for company search, profile lookup, officer and PSC listing, filing
history, and intent-to-operation mapping with UK corporate terminology
translation. Returns status-based responses (ready, needs_input, error) with
translation. Returns status-based responses (ready, partial, needs_input, error) with
disambiguation support. The agent owns natural-language dialogue; the skill
returns structured candidates and next actions.
short_description: "UK Companies House search, officers, PSC, and filings via structured actions."
Expand Down Expand Up @@ -49,6 +49,12 @@ parameters:
entities:
type: object
description: "Extracted entities for map_intent (e.g. company_query, role)."
context:
type: object
description: "State carried over from prior skill turns (e.g. company_number, company_name)."
pipeline:
type: object
description: "State of an executing multi-step pipeline (e.g. completed_steps, total_steps) to carry forward."
required:
- action
constitution: |
Expand Down
Loading
Loading