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
2 changes: 1 addition & 1 deletion .github/workflows/detect-duplicate-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
model: openai/gpt-4o
system-prompt: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/detect-non-english-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
model: openai/gpt-4o-mini
system-prompt: |
Expand Down
70 changes: 68 additions & 2 deletions homeassistant/components/ai_task/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import logging
from typing import Any

from aiohttp import web
import voluptuous as vol

from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
from homeassistant.core import (
Expand All @@ -26,22 +28,36 @@
ATTR_STRUCTURE,
ATTR_TASK_NAME,
DATA_COMPONENT,
DATA_IMAGES,
DATA_PREFERENCES,
DOMAIN,
SERVICE_GENERATE_DATA,
SERVICE_GENERATE_IMAGE,
AITaskEntityFeature,
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_http
from .task import GenDataTask, GenDataTaskResult, async_generate_data
from .task import (
GenDataTask,
GenDataTaskResult,
GenImageTask,
GenImageTaskResult,
ImageData,
async_generate_data,
async_generate_image,
)

__all__ = [
"DOMAIN",
"AITaskEntity",
"AITaskEntityFeature",
"GenDataTask",
"GenDataTaskResult",
"GenImageTask",
"GenImageTaskResult",
"ImageData",
"async_generate_data",
"async_generate_image",
"async_setup",
"async_setup_entry",
"async_unload_entry",
Expand Down Expand Up @@ -78,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
hass.data[DATA_COMPONENT] = entity_component
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
hass.data[DATA_IMAGES] = {}
await hass.data[DATA_PREFERENCES].async_load()
async_setup_http(hass)
hass.http.register_view(ImageView)
hass.services.async_register(
DOMAIN,
SERVICE_GENERATE_DATA,
Expand All @@ -101,6 +119,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
supports_response=SupportsResponse.ONLY,
job_type=HassJobType.Coroutinefunction,
)
hass.services.async_register(
DOMAIN,
SERVICE_GENERATE_IMAGE,
async_service_generate_image,
schema=vol.Schema(
{
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
),
}
),
supports_response=SupportsResponse.ONLY,
job_type=HassJobType.Coroutinefunction,
)
return True


Expand All @@ -115,11 +150,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:


async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
"""Run the run task service."""
"""Run the data task service."""
result = await async_generate_data(hass=call.hass, **call.data)
return result.as_dict()


async def async_service_generate_image(call: ServiceCall) -> ServiceResponse:
"""Run the image task service."""
return await async_generate_image(hass=call.hass, **call.data)


class AITaskPreferences:
"""AI Task preferences."""

Expand Down Expand Up @@ -164,3 +204,29 @@ def async_set_preferences(
def as_dict(self) -> dict[str, str | None]:
"""Get the current preferences."""
return {key: getattr(self, key) for key in self.KEYS}


class ImageView(HomeAssistantView):
"""View to generated images."""

url = f"/api/{DOMAIN}/images/{{filename}}"
name = f"api:{DOMAIN}/images"
requires_auth = False

async def get(
self,
request: web.Request,
filename: str,
) -> web.Response:
"""Serve image."""
hass = request.app[KEY_HASS]
image_storage = hass.data[DATA_IMAGES]
image_data = image_storage.get(filename)

if image_data is None:
raise web.HTTPNotFound

return web.Response(
body=image_data.data,
content_type=image_data.mime_type,
)
9 changes: 9 additions & 0 deletions homeassistant/components/ai_task/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@

from . import AITaskPreferences
from .entity import AITaskEntity
from .task import ImageData

DOMAIN = "ai_task"
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
DATA_IMAGES: HassKey[dict[str, ImageData]] = HassKey(f"{DOMAIN}_images")

IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
MAX_IMAGES = 20

SERVICE_GENERATE_DATA = "generate_data"
SERVICE_GENERATE_IMAGE = "generate_image"

ATTR_INSTRUCTIONS: Final = "instructions"
ATTR_TASK_NAME: Final = "task_name"
Expand All @@ -38,3 +44,6 @@ class AITaskEntityFeature(IntFlag):

SUPPORT_ATTACHMENTS = 2
"""Support attachments with generate data."""

GENERATE_IMAGE = 4
"""Generate images based on instructions."""
24 changes: 22 additions & 2 deletions homeassistant/components/ai_task/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from homeassistant.util import dt as dt_util

from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
from .task import GenDataTask, GenDataTaskResult
from .task import GenDataTask, GenDataTaskResult, GenImageTask, GenImageTaskResult


class AITaskEntity(RestoreEntity):
Expand Down Expand Up @@ -57,7 +57,7 @@ async def async_internal_added_to_hass(self) -> None:
async def _async_get_ai_task_chat_log(
self,
session: ChatSession,
task: GenDataTask,
task: GenDataTask | GenImageTask,
) -> AsyncGenerator[ChatLog]:
"""Context manager used to manage the ChatLog used during an AI Task."""
# pylint: disable-next=contextmanager-generator-missing-cleanup
Expand Down Expand Up @@ -104,3 +104,23 @@ async def _async_generate_data(
) -> GenDataTaskResult:
"""Handle a gen data task."""
raise NotImplementedError

@final
async def internal_async_generate_image(
self,
session: ChatSession,
task: GenImageTask,
) -> GenImageTaskResult:
"""Run a gen image task."""
self.__last_activity = dt_util.utcnow().isoformat()
self.async_write_ha_state()
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
return await self._async_generate_image(task, chat_log)

async def _async_generate_image(
self,
task: GenImageTask,
chat_log: ChatLog,
) -> GenImageTaskResult:
"""Handle a gen image task."""
raise NotImplementedError
3 changes: 3 additions & 0 deletions homeassistant/components/ai_task/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"services": {
"generate_data": {
"service": "mdi:file-star-four-points-outline"
},
"generate_image": {
"service": "mdi:star-four-points-box-outline"
}
}
}
2 changes: 1 addition & 1 deletion homeassistant/components/ai_task/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "ai_task",
"name": "AI Task",
"after_dependencies": ["camera"],
"after_dependencies": ["camera", "http"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["conversation", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/ai_task",
Expand Down
81 changes: 81 additions & 0 deletions homeassistant/components/ai_task/media_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Expose images as media sources."""

from __future__ import annotations

import logging

from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source import (
BrowseMediaSource,
MediaSource,
MediaSourceItem,
PlayMedia,
Unresolvable,
)
from homeassistant.core import HomeAssistant

from .const import DATA_IMAGES, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_get_media_source(hass: HomeAssistant) -> ImageMediaSource:
"""Set up image media source."""
_LOGGER.debug("Setting up image media source")
return ImageMediaSource(hass)


class ImageMediaSource(MediaSource):
"""Provide images as media sources."""

name: str = "AI Generated Images"

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize ImageMediaSource."""
super().__init__(DOMAIN)
self.hass = hass

async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
image_storage = self.hass.data[DATA_IMAGES]
image = image_storage.get(item.identifier)

if image is None:
raise Unresolvable(f"Could not resolve media item: {item.identifier}")

return PlayMedia(f"/api/{DOMAIN}/images/{item.identifier}", image.mime_type)

async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if item.identifier:
raise BrowseError("Unknown item")

image_storage = self.hass.data[DATA_IMAGES]

children = [
BrowseMediaSource(
domain=DOMAIN,
identifier=filename,
media_class=MediaClass.IMAGE,
media_content_type=image.mime_type,
title=image.title or filename,
can_play=True,
can_expand=False,
)
for filename, image in image_storage.items()
]

return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.APP,
media_content_type="",
title="AI Generated Images",
can_play=False,
can_expand=True,
children_media_class=MediaClass.IMAGE,
children=children,
)
27 changes: 27 additions & 0 deletions homeassistant/components/ai_task/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,30 @@ generate_data:
media:
accept:
- "*"
generate_image:
fields:
task_name:
example: "picture of a dog"
required: true
selector:
text:
instructions:
example: "Generate a high quality square image of a dog on transparent background"
required: true
selector:
text:
multiline: true
entity_id:
required: true
selector:
entity:
filter:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_IMAGE
attachments:
required: false
selector:
media:
accept:
- "*"
22 changes: 22 additions & 0 deletions homeassistant/components/ai_task/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@
"description": "List of files to attach for multi-modal AI analysis."
}
}
},
"generate_image": {
"name": "Generate image",
"description": "Uses AI to generate image.",
"fields": {
"task_name": {
"name": "Task name",
"description": "Name of the task."
},
"instructions": {
"name": "Instructions",
"description": "Instructions that explains the image to be generated."
},
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on."
},
"attachments": {
"name": "Attachments",
"description": "List of files to attach for using as references."
}
}
}
}
}
Loading
Loading