Skip to content
Open
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
98 changes: 81 additions & 17 deletions python/fi/prompt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def _parse_success(cls, response) -> Dict:
prompt_config_raw = pc
cfg_src = (prompt_config_raw or {}).get("configuration", {})
cfg = {
"model_name": cfg_src.get("model_name") or cfg_src.get("model"),
"model_name": cfg_src.get("model") or "unavailable",
"temperature": cfg_src.get("temperature"),
"frequency_penalty": cfg_src.get("frequency_penalty"),
"presence_penalty": cfg_src.get("presence_penalty"),
Expand All @@ -80,7 +80,7 @@ def _parse_success(cls, response) -> Dict:
"tools": cfg_src.get("tools"),
}
model_config = ModelConfig(
model_name=cfg["model_name"] or "unavailable",
model_name=cfg["model_name"],
temperature=cfg["temperature"] if cfg["temperature"] is not None else 0,
frequency_penalty=cfg["frequency_penalty"] if cfg["frequency_penalty"] is not None else 0,
presence_penalty=cfg["presence_penalty"] if cfg["presence_penalty"] is not None else 0,
Expand Down Expand Up @@ -130,7 +130,13 @@ def _handle_error(cls, response) -> None:
if response.status_code == 400:
try:
detail = response.json()
error_code = detail.get("error_code") if isinstance(detail, dict) else None
# Backend returns `code` (snake_case-style single word);
# accept `errorCode` as a legacy alternative.
error_code = (
detail.get("code")
if isinstance(detail, dict)
else None
)
except Exception:
error_code = None

Expand Down Expand Up @@ -162,7 +168,7 @@ def _dict_to_prompt_template(item: Dict) -> PromptTemplate:
pc = prompt_config_raw[0] if isinstance(prompt_config_raw, list) else prompt_config_raw
cfg_raw = pc.get("configuration", {})
cfg = {
"model_name": cfg_raw.get("model_name") or cfg_raw.get("model"),
"model_name": cfg_raw.get("model") or "unavailable",
"temperature": cfg_raw.get("temperature"),
"frequency_penalty": cfg_raw.get("frequency_penalty"),
"presence_penalty": cfg_raw.get("presence_penalty"),
Expand All @@ -173,7 +179,7 @@ def _dict_to_prompt_template(item: Dict) -> PromptTemplate:
"tools": cfg_raw.get("tools"),
}
model_config = ModelConfig(
model_name=cfg["model_name"] or "unavailable",
model_name=cfg["model_name"],
temperature=cfg["temperature"] if cfg["temperature"] is not None else 0,
frequency_penalty=cfg["frequency_penalty"] if cfg["frequency_penalty"] is not None else 0,
presence_penalty=cfg["presence_penalty"] if cfg["presence_penalty"] is not None else 0,
Expand Down Expand Up @@ -243,6 +249,22 @@ def __init__(
fi_base_url: Optional[str] = None,
**kwargs,
):
"""Initialize the Prompt client.

If ``template`` has no ``id`` but has a ``name``, the SDK will attempt
to fetch the corresponding template from the backend. This supports
two workflows:

1. Existing template — pass a ``PromptTemplate(name=...)`` and the
constructor will populate ``id``/``version`` from the backend.
2. New template — pass a ``PromptTemplate(name=..., messages=...)``
for a name that doesn't yet exist; the fetch will fail softly
(logged warning) and the user-provided template is retained with
``id=None`` so ``create()`` can register it.

For explicit retrieval use ``Prompt.get_template_by_name()`` which
raises ``TemplateNotFound`` on miss instead of falling back.
"""
super().__init__(
fi_api_key=fi_api_key,
fi_secret_key=fi_secret_key,
Expand All @@ -252,6 +274,7 @@ def __init__(

# Label requested during draft create; will be assigned on commit
self._pending_label: Optional[str] = None
self._last_generation_id: Optional[str] = None

if template and not template.id:
try:
Expand All @@ -265,7 +288,16 @@ def __init__(
self.template = template

def generate(self, requirements: str) -> "Prompt":
"""Generate a prompt and return self for chaining"""
"""Submit a prompt-generation job to the backend (asynchronous).

The backend queues the generation and returns a ``generation_id``.
The result is **not** available synchronously — there is currently no
public endpoint in the backend to poll for a generation job's output by
``generation_id``. The generated prompt is surfaced through the
FutureAGI UI / job queue rather than through this SDK.

Use ``last_generation_id`` to retrieve the id for logging / correlation.
"""
if not self.template:
raise ValueError("No template configured")
response = self.request(
Expand All @@ -274,13 +306,20 @@ def generate(self, requirements: str) -> "Prompt":
url=self._base_url + "/" + Routes.generate_prompt.value,
json={"statement": requirements},
),
response_handler=PromptResponseHandler,
response_handler=SimpleJsonResponseHandler,
)
result = response.get("result", response) if isinstance(response, dict) else response
self._last_generation_id = (
result.get("generation_id") if isinstance(result, dict) else None
)
self.template.messages[-1].content = response["result"]["prompt"]
return self

def improve(self, requirements: str) -> "Prompt":
"""Improve prompt and return self for chaining"""
"""Submit a prompt-improvement job to the backend (asynchronous).

The backend queues the improvement and returns a ``generation_id``.
See ``generate()`` for notes on async result retrieval.
"""
if not self.template:
raise ValueError("No template configured")

Expand All @@ -297,11 +336,23 @@ def improve(self, requirements: str) -> "Prompt":
"improvement_requirements": requirements,
},
),
response_handler=PromptResponseHandler,
response_handler=SimpleJsonResponseHandler,
)
result = (
improved_response.get("result", improved_response)
if isinstance(improved_response, dict)
else improved_response
)
self._last_generation_id = (
result.get("generation_id") if isinstance(result, dict) else None
)
self.template.messages[-1].content = improved_response["result"]["prompt"]
return self

@property
def last_generation_id(self) -> Optional[str]:
"""Return the generation_id from the most recent generate()/improve() call."""
return self._last_generation_id

def create(self, *, label: Optional[str] = None) -> "Prompt":
"""Create a draft prompt template and return self for chaining.

Expand Down Expand Up @@ -426,6 +477,14 @@ def delete(self) -> bool:
if not self.template or not self.template.id:
raise ValueError("Template ID missing; cannot delete.")

# Invalidate cache for this template before deleting so subsequent
# lookups don't return stale entries.
if self.template.name:
try:
prompt_cache.invalidate(self.template.name)
except Exception:
logger.warning("prompt_cache.invalidate failed during delete()", exc_info=True)

self.request(
config=RequestConfig(
method=HttpMethod.DELETE,
Expand Down Expand Up @@ -461,7 +520,7 @@ def delete_template_by_name(
tmpl: PromptTemplate = client.request(
config=RequestConfig(
method=HttpMethod.GET,
url=client._base_url + "/" + Routes.prompt_label_get_by_name.value,
url=client._base_url + "/" + Routes.get_template_by_name.value,
params={"name": name},
),
response_handler=PromptResponseHandler,
Expand All @@ -476,6 +535,10 @@ def delete_template_by_name(
),
response_handler=None,
)
try:
prompt_cache.invalidate(name)
except Exception:
logger.warning("prompt_cache.invalidate failed during delete_template_by_name()", exc_info=True)
return True
finally:
client.close()
Expand All @@ -486,7 +549,7 @@ def _fetch_template_by_name(self, name: str) -> PromptTemplate:
response = self.request(
config=RequestConfig(
method=HttpMethod.GET,
url=self._base_url + "/" + Routes.prompt_label_get_by_name.value,
url=self._base_url + "/" + Routes.get_template_by_name.value,
params={"name": name},
),
response_handler=PromptResponseHandler,
Expand Down Expand Up @@ -518,9 +581,9 @@ def _fetch_template_version_history(self):
def list_template_versions(self):
"""Return full version history as provided by the backend.

Each element in the returned list is the raw JSON entry that includes
at least these keys: ``template_version``, ``is_draft`` and
``created_at``.
Each element in the returned list is the raw JSON entry. The backend
returns snake_case keys (``template_version``, ``is_draft``,
``created_at``); callers that need camelCase should normalize.
"""
return self._fetch_template_version_history()

Expand All @@ -532,7 +595,8 @@ def _current_version_is_draft(self) -> bool:
"""Check backend state to know if the current version is still draft."""
history = self._fetch_template_version_history()
for entry in history:
if entry.get("template_version") == self.template.version:
entry_version = entry.get("template_version")
if entry_version == self.template.version:
return bool(entry.get("is_draft"))
# If not found assume draft (conservative)
return True
Expand Down
10 changes: 6 additions & 4 deletions python/fi/prompt/label_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ def _assign_label_to_template_version_by_names(self, template_name: str, version
history = history_resp.json().get("results", [])
matched = None
for entry in history:
if str(entry.get("template_version")) == version:
entry_version = entry.get("template_version")
if str(entry_version) == version:
matched = entry
break
if not matched:
Expand Down Expand Up @@ -328,7 +329,8 @@ def _remove_label_from_template_version_by_names(self, template_name: str, versi
history = history_resp.json().get("results", [])
version_id = None
for entry in history:
if str(entry.get("template_version")) == version:
entry_version = entry.get("template_version")
if str(entry_version) == version:
for key in ("id", "version_id", "execution_id"):
if entry.get(key):
version_id = str(entry.get(key))
Expand Down Expand Up @@ -357,8 +359,8 @@ def _get_version_id_by_name(self, version_name: str) -> Optional[str]:
"""Lookup internal version_id by version name via history endpoint."""
history = self._fetch_template_version_history()
for entry in history:
if str(entry.get("template_version")) == version_name:
# Try common id keys
entry_version = entry.get("template_version")
if str(entry_version) == version_name:
for key in ("id", "version_id", "execution_id"):
if entry.get(key):
return str(entry[key])
Expand Down
Loading