Initial commit
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"""Talent market service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from opc.layer2_organization.talent_market import TalentMarket
|
||||
|
||||
from .context import OfficeServiceContext
|
||||
from .models import ServiceError, ServiceEvent, ServiceResult
|
||||
|
||||
|
||||
class TalentService:
|
||||
def __init__(self, context: OfficeServiceContext) -> None:
|
||||
self.context = context
|
||||
|
||||
def _ensure_custom_org_editable(self) -> None:
|
||||
if not self.context.is_custom_org_editable():
|
||||
raise ServiceError(
|
||||
"org_read_only",
|
||||
"Corporate organization is read-only. Select or create a saved custom org before editing.",
|
||||
)
|
||||
|
||||
@property
|
||||
def market(self) -> TalentMarket:
|
||||
return TalentMarket(self.context.opc_home, self.context.engine.config)
|
||||
|
||||
async def list(self) -> ServiceResult:
|
||||
market = self.market
|
||||
templates = list(market.list_templates())
|
||||
known = {getattr(item, "id", "") for item in templates}
|
||||
try:
|
||||
for item in market.scan_local_talent():
|
||||
if getattr(item, "id", "") not in known:
|
||||
templates.append(item)
|
||||
known.add(getattr(item, "id", ""))
|
||||
except Exception:
|
||||
pass
|
||||
payloads = [self._template_payload(item) for item in templates]
|
||||
try:
|
||||
from opc.market.talent_presets import get_all_talent_presets
|
||||
for preset in get_all_talent_presets():
|
||||
preset_id = str(preset.get("id", "") or "")
|
||||
if preset_id and preset_id not in known:
|
||||
payloads.append({
|
||||
"template_id": preset_id,
|
||||
"id": preset_id,
|
||||
"name": preset.get("name", preset_id),
|
||||
"description": preset.get("description", ""),
|
||||
"category": preset.get("category", ""),
|
||||
"domains": list(preset.get("domains", []) or []),
|
||||
"tags": list(preset.get("tags", []) or []),
|
||||
"preferred_external_agent": preset.get("preferred_external_agent"),
|
||||
"source_repo": "builtin",
|
||||
"emoji": preset.get("emoji", ""),
|
||||
"color": preset.get("color", ""),
|
||||
"vibe": preset.get("vibe", ""),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
payloads.sort(key=lambda item: (str(item.get("category", "")), str(item.get("name", "")).lower()))
|
||||
return ServiceResult({"templates": payloads, "talent_dir": str(market.opc_home / "prompts" / "talent")})
|
||||
|
||||
async def employees(self) -> ServiceResult:
|
||||
employees = self.market.list_employees()
|
||||
return ServiceResult({"employees": [self._employee_payload(item) for item in employees]})
|
||||
|
||||
async def scan(self) -> ServiceResult:
|
||||
templates = self.market.scan_local_talent()
|
||||
return ServiceResult({"templates": [self._template_payload(item) for item in templates]})
|
||||
|
||||
async def import_repo(self, path: str) -> ServiceResult:
|
||||
self._ensure_custom_org_editable()
|
||||
repo_path = Path(path).expanduser().resolve()
|
||||
if not repo_path.is_dir():
|
||||
raise ServiceError("directory_not_found", f"directory not found: {path}", {"path": path})
|
||||
imported = self.market.import_from_repo(repo_path)
|
||||
self._persist_config()
|
||||
return ServiceResult({"action": "talent_imported", "imported": [self._template_payload(item) for item in imported], "count": len(imported)})
|
||||
|
||||
async def import_selected(self, template_ids: list[str]) -> ServiceResult:
|
||||
self._ensure_custom_org_editable()
|
||||
if not template_ids:
|
||||
raise ServiceError("missing_template_ids", "No templates selected")
|
||||
imported = self.market.import_local_templates(template_ids)
|
||||
self._persist_config()
|
||||
return ServiceResult({"count": len(imported), "imported": [self._template_payload(item) for item in imported]})
|
||||
|
||||
async def _load_target_org_for_hire(self, organization_id: str | None = None) -> None:
|
||||
target_org_id = str(organization_id or "").strip()
|
||||
if not target_org_id and self.context.get_active_saved_org_name is not None:
|
||||
try:
|
||||
target_org_id = str(await self.context.get_active_saved_org_name() or "").strip()
|
||||
except Exception:
|
||||
target_org_id = ""
|
||||
if not target_org_id:
|
||||
return
|
||||
|
||||
cfg_org = getattr(getattr(self.context.engine, "config", None), "org", None)
|
||||
current_org_id = str(getattr(cfg_org, "organization_id", "") or "").strip()
|
||||
current_profile = str(getattr(cfg_org, "company_profile", "") or "").strip().lower()
|
||||
if current_org_id == target_org_id and current_profile == "custom":
|
||||
return
|
||||
if self.context.load_active_org_config is None:
|
||||
return
|
||||
try:
|
||||
loaded = self.context.load_active_org_config(target_org_id)
|
||||
except Exception as exc:
|
||||
raise ServiceError(
|
||||
"saved_org_load_failed",
|
||||
f"Failed to load organization '{target_org_id}' before hiring.",
|
||||
{"organization_id": target_org_id},
|
||||
) from exc
|
||||
if not loaded:
|
||||
raise ServiceError(
|
||||
"saved_org_not_found",
|
||||
f"Organization '{target_org_id}' is not available for hiring.",
|
||||
{"organization_id": target_org_id},
|
||||
)
|
||||
|
||||
async def hire(
|
||||
self,
|
||||
*,
|
||||
template_id: str,
|
||||
role_id: str,
|
||||
employee_name: str | None = None,
|
||||
employee_id: str | None = None,
|
||||
organization_id: str | None = None,
|
||||
) -> ServiceResult:
|
||||
await self._load_target_org_for_hire(organization_id)
|
||||
self._ensure_custom_org_editable()
|
||||
if not template_id or not role_id:
|
||||
raise ServiceError("missing_hire_fields", "template_id and role_id required")
|
||||
role_exists = any(
|
||||
str(getattr(role, "id", getattr(role, "role_id", "")) or "") == role_id
|
||||
for role in getattr(self.context.engine.config.org, "roles", []) or []
|
||||
)
|
||||
org = getattr(self.context.engine, "org_engine", None)
|
||||
if not role_exists and org and hasattr(org, "get_agent"):
|
||||
role_exists = bool(org.get_agent(role_id))
|
||||
if not role_exists:
|
||||
raise ServiceError("role_not_found", f"Role '{role_id}' does not exist", {"role_id": role_id})
|
||||
displaced_placeholder_ids = [
|
||||
item.employee_id
|
||||
for item in getattr(self.context.engine.config.org, "employees", []) or []
|
||||
if item.role_id == role_id
|
||||
and (
|
||||
dict(getattr(item, "metadata", {}) or {}).get("is_default_employee")
|
||||
or dict(getattr(item, "metadata", {}) or {}).get("is_fallback_employee")
|
||||
)
|
||||
]
|
||||
try:
|
||||
employee = self.market.hire_template(template_id, role_id, employee_name=employee_name, employee_id=employee_id)
|
||||
self._persist_config()
|
||||
except Exception as exc:
|
||||
raise ServiceError("talent_hire_failed", str(exc), {"template_id": template_id, "role_id": role_id}) from exc
|
||||
|
||||
for displaced_id in displaced_placeholder_ids:
|
||||
try:
|
||||
remover = getattr(self.context.agent_store, "remove_agent", None)
|
||||
if callable(remover):
|
||||
await remover(f"emp-{displaced_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
employee_payload = self._employee_payload(employee)
|
||||
payload: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"action": "talent_hired",
|
||||
"employee_id": employee.employee_id,
|
||||
"name": employee.name,
|
||||
"role_id": employee.role_id,
|
||||
"employee": employee_payload,
|
||||
}
|
||||
events: list[ServiceEvent] = []
|
||||
if self.context.mode_state.exec_mode in {"org", "custom"}:
|
||||
try:
|
||||
agents: list[dict[str, Any]] = []
|
||||
if self.context.ensure_custom_role_agents is not None:
|
||||
agents = list(await self.context.ensure_custom_role_agents() or [])
|
||||
else:
|
||||
creator = getattr(self.context.agent_store, "create_agent_from_employee", None)
|
||||
if callable(creator):
|
||||
await creator(employee_payload)
|
||||
if self.context.sync_role_map is not None:
|
||||
await self.context.sync_role_map()
|
||||
sync = getattr(self.context.agent_store, "sync_custom_shadow", None)
|
||||
if callable(sync):
|
||||
await sync()
|
||||
if not agents:
|
||||
getter = getattr(self.context.agent_store, "get_all", None)
|
||||
agents = await getter() if callable(getter) else []
|
||||
events.append(ServiceEvent("ack", {"ok": True, "action": "agent_spawned", "agents": agents}))
|
||||
payload["deploy_ok"] = True
|
||||
except Exception as exc:
|
||||
payload["deploy_ok"] = False
|
||||
payload["deploy_error"] = str(exc)
|
||||
return ServiceResult(payload, events)
|
||||
|
||||
async def employee_detail(self, employee_id: str) -> ServiceResult:
|
||||
employee = next((item for item in self.market.list_employees() if item.employee_id == employee_id), None)
|
||||
if not employee:
|
||||
raise ServiceError("employee_not_found", "Employee not found", {"employee_id": employee_id})
|
||||
payload = self._employee_payload(employee)
|
||||
org = getattr(self.context.engine, "org_engine", None)
|
||||
evolution = getattr(org, "employee_evolution", None) if org else None
|
||||
if evolution:
|
||||
try:
|
||||
payload["experience_score"] = evolution.get_experience_score(
|
||||
employee.employee_id,
|
||||
role_id=employee.role_id,
|
||||
domains=list(getattr(employee, "domains", []) or []),
|
||||
)
|
||||
payload["learned_skill_refs"] = evolution.get_learned_skill_refs(employee.employee_id)
|
||||
payload["delta_context"] = evolution.build_employee_delta_context(employee.employee_id)
|
||||
payload["profile"] = evolution.get_employee_profile(employee.employee_id)
|
||||
except Exception:
|
||||
pass
|
||||
return ServiceResult({"employee": payload})
|
||||
|
||||
async def import_employee_as_agent(self, *, employee_id: str, office_id: str = "office-0") -> ServiceResult:
|
||||
from .agent import AgentService
|
||||
|
||||
return await AgentService(self.context).import_employee(employee_id=employee_id, office_id=office_id)
|
||||
|
||||
def _persist_config(self) -> None:
|
||||
if self.context.persist_runtime_config is not None:
|
||||
self.context.persist_runtime_config()
|
||||
else:
|
||||
self.context.engine.config.save()
|
||||
|
||||
@staticmethod
|
||||
def _template_payload(template: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"template_id": getattr(template, "id", ""),
|
||||
"id": getattr(template, "id", ""),
|
||||
"name": getattr(template, "name", ""),
|
||||
"description": getattr(template, "description", ""),
|
||||
"category": getattr(template, "category", ""),
|
||||
"domains": list(getattr(template, "domains", []) or []),
|
||||
"tags": list(getattr(template, "tags", []) or []),
|
||||
"preferred_external_agent": getattr(template, "preferred_external_agent", None),
|
||||
"source_repo": getattr(template, "source_repo", ""),
|
||||
"emoji": getattr(template, "emoji", "") or "",
|
||||
"color": getattr(template, "color", "") or "",
|
||||
"vibe": getattr(template, "vibe", "") or "",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _employee_payload(employee: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"employee_id": getattr(employee, "employee_id", ""),
|
||||
"name": getattr(employee, "name", ""),
|
||||
"role_id": getattr(employee, "role_id", ""),
|
||||
"category": getattr(employee, "category", ""),
|
||||
"domains": list(getattr(employee, "domains", []) or []),
|
||||
"seniority": getattr(employee, "seniority", "junior"),
|
||||
"status": getattr(employee, "status", "active"),
|
||||
"tags": list(getattr(employee, "tags", []) or []),
|
||||
"prompt_refs": list(getattr(employee, "prompt_refs", []) or []),
|
||||
"skill_refs": list(getattr(employee, "skill_refs", []) or []),
|
||||
"preferred_external_agent": getattr(employee, "preferred_external_agent", None),
|
||||
}
|
||||
Reference in New Issue
Block a user