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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,31 @@ class StarNewsFinder {

---

### 🌐 **Beginner: Company Namer**

> **Available in:** Java | **Concept:** Combining generative and deterministic actions

Suggests company names from a business brief, then checks each matching `.com` domain using Java's DNS resolver.
The check requires no third-party availability API or API key: domains with DNS records are reported as in use, while
domains without DNS records are reported as potentially available and must be confirmed with a registrar.

**What It Teaches:**

- Generating structured candidate data with an LLM
- Passing generated data to a deterministic action
- Keeping external checks behind an injected service
- Caching DNS results and representing inconclusive checks honestly

**Try It:**

```bash
x "Suggest company names for a sustainable accounting platform"
```

**Location:** `examples-java/src/main/java/com/embabel/example/companynamer/`

---

### 🔬 **Expert: Multi-LLM Research Agent**

> **Available in:** Java, Kotlin | **Concept:** Self-Improving AI Workflows
Expand Down
25 changes: 23 additions & 2 deletions examples-java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ examples-java/
└── application.yml # Configuration
```

## 🎯 Available Example
## 🎯 Available Examples

### **Horoscope News Finder** (Beginner-Friendly)

Expand Down Expand Up @@ -92,6 +92,27 @@ shell:> execute "Find horoscope news for Bob who is a Scorpio"
shell:> execute "What's in the stars for Alice today? She's a Gemini"
```

### **Company Namer** (Beginner-Friendly)

Suggest company names from a business brief and perform a deterministic DNS check for each matching `.com` domain.

```text
shell:> execute "Suggest company names for a sustainable accounting platform"
```

The example deliberately uses the JDK DNS resolver instead of a domain-availability API, so it needs no additional API
key and does not consume a provider quota. A domain with DNS records is reported as `IN_USE`. A domain with no DNS
records is only `POTENTIALLY_AVAILABLE`, because registered domains do not have to publish DNS records; always confirm
the final choice with a registrar.

The workflow demonstrates structured generation followed by deterministic processing:

1. `CompanyNamer.suggestNames` creates eight names, domains, and rationales.
2. `CompanyNamer.checkDomains` delegates each lookup to `DomainAvailabilityService`.
3. The service validates and normalizes domains, caches results, and distinguishes conclusive from inconclusive checks.

**Location:** `src/main/java/com/embabel/example/companynamer/`

## 🛠️ Configuration

### API Keys
Expand Down Expand Up @@ -311,4 +332,4 @@ When adding new Java examples:

## 📄 License

Licensed under Apache License 2.0. See [LICENSE](../../LICENSE) for details.
Licensed under Apache License 2.0. See [LICENSE](../../LICENSE) for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.companynamer;

import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Agent;
import com.embabel.agent.api.annotation.Export;
import com.embabel.agent.api.common.Ai;
import com.embabel.agent.domain.io.UserInput;

import java.util.List;

record CompanyNameIdea(String name, String domain, String rationale) {
}

record CompanyNameIdeas(List<CompanyNameIdea> ideas) {
}

record CheckedCompanyName(String name, String rationale, DomainAvailability domainAvailability) {
}

record CompanyNamingReport(List<CheckedCompanyName> suggestions) {
}

/**
* Suggests company names and performs quota-free DNS checks for their domains.
*/
@Agent(
name = "JavaCompanyNamer",
description = "Suggest company names and check whether their domain names have DNS records",
beanName = "javaCompanyNamer")
public class CompanyNamer {

private final DomainAvailabilityService domainAvailabilityService;

public CompanyNamer(DomainAvailabilityService domainAvailabilityService) {
this.domainAvailabilityService = domainAvailabilityService;
}

@Action
CompanyNameIdeas suggestNames(UserInput userInput, Ai ai) {
return ai.withDefaultLlm().createObject(
"""
Suggest 8 memorable company names for the following brief:
<brief>%s</brief>

For each suggestion, provide:
- the company name
- a concise rationale
- one matching fully qualified .com domain name, using only letters, digits, and hyphens

Make every company name and domain distinct. Domain availability will be checked separately,
so do not claim that a domain is available.
""".formatted(userInput.getContent()),
CompanyNameIdeas.class);
}

@AchievesGoal(
description = "Suggest company names with matching domain-name checks",
export = @Export(
remote = true,
name = "nameCompanyJava",
startingInputTypes = {UserInput.class}))
@Action
CompanyNamingReport checkDomains(CompanyNameIdeas companyNameIdeas) {
var checkedNames = companyNameIdeas.ideas().stream()
.map(idea -> new CheckedCompanyName(
idea.name(),
idea.rationale(),
domainAvailabilityService.check(idea.domain())))
.toList();
return new CompanyNamingReport(checkedNames);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.companynamer;

public record DomainAvailability(
String domain,
Status status,
String explanation) {

public enum Status {
IN_USE,
POTENTIALLY_AVAILABLE,
UNKNOWN
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.companynamer;

import org.springframework.stereotype.Service;

import java.net.IDN;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

@Service
public class DomainAvailabilityService {

private static final Pattern DOMAIN_PATTERN = Pattern.compile(
"(?=.{1,253}\\.?$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?");

private final DnsResolver dnsResolver;
private final Map<String, DomainAvailability> cache = new ConcurrentHashMap<>();

public DomainAvailabilityService() {
this(InetAddress::getAllByName);
}

DomainAvailabilityService(DnsResolver dnsResolver) {
this.dnsResolver = dnsResolver;
}

public DomainAvailability check(String domain) {
String normalizedDomain;
try {
normalizedDomain = normalize(domain);
} catch (IllegalArgumentException ex) {
return new DomainAvailability(
domain,
DomainAvailability.Status.UNKNOWN,
"The proposed domain name is invalid: " + ex.getMessage());
}
return cache.computeIfAbsent(normalizedDomain, this::resolve);
}

private DomainAvailability resolve(String domain) {
try {
dnsResolver.resolve(domain);
return new DomainAvailability(
domain,
DomainAvailability.Status.IN_USE,
"The domain has DNS records and is already in use.");
} catch (UnknownHostException ex) {
return new DomainAvailability(
domain,
DomainAvailability.Status.POTENTIALLY_AVAILABLE,
"No DNS records were found. Confirm availability with a registrar before purchasing.");
} catch (Exception ex) {
return new DomainAvailability(
domain,
DomainAvailability.Status.UNKNOWN,
"The DNS lookup could not be completed. Try again or check with a registrar.");
}
}

private String normalize(String domain) {
if (domain == null || domain.isBlank()) {
throw new IllegalArgumentException("domain must not be blank");
}
var normalized = IDN.toASCII(domain.strip())
.toLowerCase(Locale.ROOT)
.replaceFirst("\\.$", "");
if (!DOMAIN_PATTERN.matcher(normalized).matches()) {
throw new IllegalArgumentException("expected a fully qualified domain name");
}
return normalized;
}

@FunctionalInterface
interface DnsResolver {
InetAddress[] resolve(String domain) throws Exception;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.companynamer;

import com.embabel.agent.domain.io.UserInput;
import com.embabel.agent.test.unit.FakeOperationContext;
import org.junit.jupiter.api.Test;

import java.net.UnknownHostException;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CompanyNamerTest {

@Test
void namingPromptContainsTheBriefAndDoesNotAssumeAvailability() {
var context = new FakeOperationContext();
context.expectResponse(new CompanyNameIdeas(List.of(
new CompanyNameIdea("Bright Forge", "brightforge.com", "Memorable"))));
var companyNamer = new CompanyNamer(new DomainAvailabilityService());

companyNamer.suggestNames(new UserInput("A sustainable accounting platform"), context.ai());

var prompt = context.getLlmInvocations().getFirst().getMessages().getFirst().getContent();
assertTrue(prompt.contains("A sustainable accounting platform"));
assertTrue(prompt.contains("do not claim that a domain is available"));
}

@Test
void checksEverySuggestedDomain() {
var checkedDomains = new java.util.ArrayList<String>();
var service = new DomainAvailabilityService(domain -> {
checkedDomains.add(domain);
throw new UnknownHostException(domain);
});
var companyNamer = new CompanyNamer(service);
var ideas = new CompanyNameIdeas(List.of(
new CompanyNameIdea("Bright Forge", "brightforge.com", "Memorable"),
new CompanyNameIdea("Green Ledger", "greenledger.com", "Descriptive")));

var report = companyNamer.checkDomains(ideas);

assertEquals(List.of("brightforge.com", "greenledger.com"), checkedDomains);
assertEquals(2, report.suggestions().size());
}
}
Loading